Promise因為它的呼叫方式使得非同步操作清晰簡單,是現在非同步操作的主要方式。Promise的使用和實現是面試中的高頻問點。這篇文章主要解析Promise規範和一版實現方式。
PromiseA+規範
瞭解Promise首先我們要清楚Promise規範的內容,規範規定了Promise的行為和呼叫方式。這裡是規範原文。下面是翻譯總結:
一個Promise主要代表了一個非同步操作的最終結果。與Promise互動的主要方式是通過promise例項的then
方法註冊回撥函式。回撥函式分為兩種,一種接受非同步操作成功時的回撥,接受非同步操作結果值作為引數;第二種是非同步操作失敗時的回撥,接受非同步操作失敗原因為引數。規範主要是詳細描述了then
方法的實現細節。
主要要求如下:
狀態
一個promise必須處於這3種狀態之中:pending
、fullfilled
、rejected
pending
: 可以轉換到fullfilled
或者rejected
狀態fullfilled
: 不能轉換到任何狀態且有一個不可變的結果值rejected
: 不能轉換到任何狀態且有一個不可變的失敗原因
then
方法
一個promise
必須有一個then
方法來接受非同步操作的結果值或者失敗原因
一個promise
的then應該接收兩個引數,成功回撥函式和失敗回撥函式:
promise.then(onFulfilled, onRejected)
複製程式碼
-
onFulfilled和onRejected都是可選的:
- 如果onFulfilled不是一個函式,它必須被忽略
- 如果onRejected不是一個函式,它必須被忽略
-
如果onFulfilled是函式:
- 它必須在
promise
狀態變為fullfilled
之後執行,且接受promise
的結果值作為第一個引數 - 它在
promise
狀態變為fullfilled
之前不能執行 - 它最多被執行一次
- 它必須在
-
如果onRejected是函式:
- 它必須在
promise
狀態變為rejected
之後執行,且接受promise
的失敗原因作為第一個引數 - 它在
promise
狀態變為rejected
之前不能執行 - 它最多被執行一次
- 它必須在
-
onFulfilled
或者onRejected
直到執行環境堆疊只包含平臺程式碼時才可以執行 -
onFulfilled
或者onRejected
必須作為一個獨立的函式被呼叫。(不能作為物件屬性執行或者其他指定this的執行方式,比如call和apply) -
then
方法可以在同一個promise
上多次呼叫:- 當
promise
狀態變為fullfilled
時,通過then註冊的所有onFulfilled
回撥按照註冊順序依次執行 - 當
promise
狀態變為rejected
時,通過then註冊的所有onRejected
回撥按照註冊順序依次執行
- 當
-
then
方法必須返回一個promise:promise2 = promise1.then(onFulfilled, onRejected); 複製程式碼
- 如果
onFulfilled
或者onRejected
返回x
, 則執行Pomise Resolution Procedure[[Resolve]](promise2, x)
- 如果
onFulfilled
或者onRejected
丟擲一個異常e
,promise2
必須以e
作為失敗原因變成rejected
狀態 - 如果
onFulfilled
不是一個函式,且promise1
為fullfilled
狀態,則promise2
也轉變為fullfilled
,且結果值和promise1
一樣 - 如果
onRejected
不是一個函式,且promise1
為rejected
狀態,則promise2
也轉變為rejected
,且失敗原因和promise1
一樣
- 如果
The Promise Resolution Procedure
promise resolution procedure
表示為[[Resolve]](promise, x)
,是一個接受一個promise和一個值作為引數的抽象操作。如果一個函式是一個thenable
(thenable
表示有then
方法的物件或者函式),則它試圖使promise
採用x
的狀態。這裡對於thenable
的處理使Promise更加通用,能夠相容之前並不符合規範但是有合理then
方法的非同步實現。
為了執行[[Resolve]](promise, x)
,需要執行以下步驟:
- 如果
promise
和x
指向同一個物件,則以TypeError
為失敗原因將promise
轉變為rejected
狀態 - 如果
x
是一個promise,則promise
採用x
的狀態以及相應的結果值或者原因 - 如果
x
是一個物件或者函式Let
定義then
,值為x.then
- 如果在獲取
x.then
的過程中丟擲異常e
,promise
以e
作為失敗原因變成rejected
狀態 - 如果
then
是一個函式,以x
作為它的this
呼叫它,第一個引數為resolvePromise
,第二個引數為rejectPromise
:- 如果
resolvePromise
以引數y
被呼叫的話,則執行[[Resolve]](promise, y)
- 如果
rejectPromise
以引數r
被呼叫的話,則以r
作為失敗原因使promise
變成rejected
狀態 - 如果
resolvePromise
和rejectPromise
都被呼叫了,或者以同樣的引數被呼叫多次,則以第一次呼叫為準,忽略之後的呼叫 - 如果呼叫
then
時丟擲一個異常e
:- 如果
resolvePromise
或rejectPromise
被呼叫了,則忽略 - 否則,
promise
以e
作為失敗原因變成rejected
狀態
- 如果
- 如果
- 如果
then
不是一個函式,則promise
以x
為結果值變為fullfilled
狀態
- 如果
x
不是一個物件或者函式,則promise
以x
為結果值變為fullfilled
狀態
Promise實現
我們可以看出標準還是挺複雜的,我們就一步一步,從簡到繁地實現Promise。標準中並沒有規定Promise如何建立,如何完成狀態轉換,這些我們可以參考ES6的promise用法:
const promise1 = new Promise((resolve, reject) => {
});
promise1.then((value) => {}, (reason)=>{})
複製程式碼
promise的狀態
Promise以類的方式出現,建構函式接受一個函式fn
作為引數,fn
有兩個引數:
resolve
,使promise從pending
狀態轉換為fullfilled
狀態,resolve(value)
的引數value為promise的結果值;reject
,使promise從pending
狀態轉換為rejected
狀態,reject(reason)
的引數reason為promise的失敗原因。
我們第一版可以先從狀態寫起:
const PENDING = 'pending';
const FULLFILLED = 'fullfilled';
const REJECTED = 'rejected';
class Promise {
constructor(fn) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (value) => {
if(this.status === PENDING){
this.status = FULLFILLED;
this.value = value;
}
}
const reject = (reason) => {
if(this.status === PENDING){
this.status = REJECTED;
this.reason = reason;
}
}
try{
fn(resolve, reject);
}catch(e){
reject(e);
}
}
}
複製程式碼
try...catch...是為了捕獲使用者自定義函式fn
的錯誤,如果fn
執行出錯,則promise會進入rejected
狀態。
then函式的兩個函式引數的執行
接下來我們考慮then
函式,then
函式比較複雜,我們先考慮then(onFulfilled, onRejected)
函式中兩個函式引數的執行問題,把then
函式的返回值問題放在後面。
then
函式中兩個函式引數的執行情況分為3種:
-
promise處於
pending
狀態,那在then函式中不執行兩個函式引數,等到fn
中的非同步操作有結果了,再根據成功或者失敗的結果去執行 -
promise處於
fullfilled
狀態,則直接執行then
函式的第一個函式引數onFulfilled
-
promise處於
rejected
狀態,則直接執行then
函式的第二個函式引數onRejected
class Promise {
constructor(fn) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.fullFilledCallbacks = []; // 儲存狀態轉變之前的註冊的onFulfilled
this.rejectedCallbacks = []; // 儲存狀態轉變之前的註冊的onRejected
const resolve = (value) => {
if(this.status === PENDING){
this.status = FULLFILLED;
this.value = value;
this.fullFilledCallbacks.forEach(callback => callback(value));
}
}
const reject = (reason) => {
if(this.status === PENDING){
this.status = REJECTED;
this.reason = reason;
this.rejectedCallbacks.forEach(callback => callback(reason));
}
}
try{
fn(resolve, reject);
}catch(e){
reject(e);
}
}
then(onFulfilled, onRejected){
if(typeof onFullfilled !== 'function'){
onFullfilled = () => {};
}
if(typeof onRejected !== 'function'){
onRejected = () => {};
}
if (this.status === 'fullfilled'){
onFulfilled(this.value);
} else if (this.status === 'rejected'){
onRejected(this.reason);
} else {
this.fullFilledCallbacks.push(onFulfilled);
this.rejectedCallbacks.push(onRejected);
}
}
}
複製程式碼
以上程式碼很簡單,我們藉助fullFilledCallbacks
和rejectedCallbacks
兩個陣列儲存在promise狀態為發生轉變之前通過then
方法註冊的onFulfilled
和onRejected
回撥。
then的返回值
由PromiseA+規範可以,then
方法必須返回一個promise:
promise2 = promise1.then(onFulfilled, onRejected);
複製程式碼
且promise2的狀態由onFullfilled
或者onRejected
的返回值決定,如何進行轉換則由promise resolution procedure
方法來實現。我們先不考慮promise resolution procedure
,根據規範實現如下:
then(onFulfilled, onRejected){
let _resolve;
let _reject;
const promise2 = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
// 如果`onFulfilled`不是一個函式,且`promise1`為`fullfilled`狀態,則`promise2`也轉變為`fullfilled`,且結果值和`promise1`一樣
if(typeof onFullfilled !== 'function'){
onFullfilled = (value) => value;
}
// 如果`onRejected`不是一個函式,且`promise1`為`rejected`狀態,則`promise2`也轉變為`rejected`,且失敗原因和`promise1`一樣
if(typeof onRejected !== 'function'){
onRejected = (reason) => throw new Error(reason);
}
// 如果`onFulfilled`或者`onRejected`返回`x`, 則執行Pomise Resolution Procedure`[[Resolve]](promise2, x)`
const excuteFullfilled = () => {
const x = onFulfilled(this.value);
this.promiseResolution(promise2, x, _resolve, _reject);
}
const excuteRejected = () => {
const x = onRejected(this.reason);
this.promiseResolution(promise2, x, _resolve, _reject);
}
// 如果`onFulfilled`或者`onRejected`丟擲一個異常`e`,`promise2`必須以`e`作為失敗原因變成`rejected`狀態
try{
if (this.status === 'fullfilled'){
excuteFullfilled();
} else if (this.status === 'rejected'){
excuteRejected();
} else {
this.fullFilledCallbacks.push(() => {
try{
excuteFullfilled();
}catch(e){
_reject(e);
}
});
this.rejectedCallbacks.push(() => {
try{
excuteRejected();
}catch(e){
_reject(e);
}
});
}
}catch(e){
_reject(e);
}
return promise2;
}
promiseResolution(){
...
}
複製程式碼
接下來我們來實現promiseResolution
函式,如果已經忘了可以去複習一下promiseResolution
的規範。promiseResolution
的主要任務是根據onFulFilled
或者onRejected
的返回值x
來決定promise2
的狀態轉變。
promiseResolution(promise, x, resolve, reject){
// 如果`resolvePromise`和`rejectPromise`都被呼叫了,或者以同樣的引數被呼叫多次,則以第一次呼叫為準,忽略之後的呼叫
let called = false;
// 如果`resolvePromise`以引數`y`被呼叫的話,則執行`[[Resolve]](promise, y)`
const resolvePromise = (y) => {
if(!called){
this.promiseResolution(promise, y, resolve, reject);
called = true;
}
}
// 如果`rejectPromise`以引數`r`被呼叫的話,則以`r`作為失敗原因使``變成`rejected`狀態
const rejectPromise = (r) => {
f(!called){
reject(r);
called = true;
}
}
if(promise === x){
throw new Error('TypeError');
}
// 如果`x`是一個promise,則`promise`採用`x`的狀態以及相應的結果值或者原因
if( x instanceof Promise){
x.then(resovle, reject)
}
if( x && (typeof x === 'object' || typeof x === 'function')){
let then = x.then;
if(typeof then === 'function'){
try{
then.call(x, resolvePromise, rejectPromise);
}catch(e){
if(!called){
reject(e);
}
}
}else{
// 如果`then`不是一個函式,則`promise`以`x`為結果值變為`fullfilled`狀態
resolve(x);
}
} else {
// 如果`x`不是一個物件或者函式,則`promise`以`x`為結果值變為`fullfilled`狀態
resolve(x);
}
}
複製程式碼
非同步問題
規範中有一條時,onFulfilled
或者onRejected
直到執行環境堆疊只包含平臺程式碼時才可以執行。規範給出的註釋是:平臺程式碼是指引擎、執行環境和Promise實現程式碼,就是說js主棧中不能有其他程式碼,這就是要求我們非同步執行onFulfilled
或者onRejected
;我們可以用巨集任務(setTimeout 或者 setImmediate)或者微任務(MutationObserver or process.nextTick)機制來實現。
我們知道,ES6的promise的then方法回撥非同步執行的機制是微任務。在瀏覽器環境我們可以使用MutationObserver來模擬微任務機制,如果瀏覽器不支援MutationObserver
,我們回退到setTimeout使用巨集任務實現。
function isNative(fn){
return fn.toString.includes('native code');
}
function asynTask(){
if(typeof MutationObserver === 'function' && isNative(MutationObserver)){
return (fn) => {
var targetNode = document.createElement('div');
var config = { attributes: true };
// Create an observer instance linked to the callback function
var observer = new MutationObserver(fn);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
targetNode.id = 'anyway';
}
} else if(typeof setImmediate === 'function' && isNative(setImmediate)){
return (fn) => {setImmediate(fn)};
} else{
return (fn) => {setTimeout(fn, 0)};
}
}
class Promise{
registerAsyn: asynExcute()
}
複製程式碼
我們使用非同步機制來重寫then
then(onFulfilled, onRejected){
let _resolve;
let _reject;
const promise2 = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
// 如果`onFulfilled`不是一個函式,且`promise1`為`fullfilled`狀態,則`promise2`也轉變為`fullfilled`,且結果值和`promise1`一樣
if(typeof onFullfilled !== 'function'){
onFullfilled = (value) => value;
}
// 如果`onRejected`不是一個函式,且`promise1`為`rejected`狀態,則`promise2`也轉變為`rejected`,且失敗原因和`promise1`一樣
if(typeof onRejected !== 'function'){
onRejected = (reason) => throw new Error(reason);
}
// 如果`onFulfilled`或者`onRejected`返回`x`, 則執行Pomise Resolution Procedure`[[Resolve]](promise2, x)`
const excuteFullfilled = () => {
try{
const x = onFulfilled(this.value);
this.promiseResolution(promise2, x, _resolve, _reject);
}catch(e){
_reject(e);
}
}
const excuteRejected = () => {
try{
const x = onRejected(this.reason);
this.promiseResolution(promise2, x, _resolve, _reject);
}catch(e){
_reject(e);
}
}
if (this.status === 'fullfilled'){
registerAysn(excuteFullfilled);
} else if (this.status === 'rejected'){
registerAysn(excuteRejected);
} else {
this.fullFilledCallbacks.push(() => {
registerAysn(excuteFullfilled);
});
this.rejectedCallbacks.push(() => {
registerAysn(excuteRejected);
});
}
return promise2;
}
複製程式碼
目前為止我們實現了Promise的基本功能。
Promise靜態方法和其他例項方法
class Promise{
catch(fn) {
return this.then((null, fn);
}
finally(fn) {
const P = this.constructor;
return this.then(
value => P.resolve(fn()).then(() => value),
reason => P.resolve(fn()).then(() => { throw reason })
);
}
}
//resolve方法
Promise.resolve = function(value) {
if (value instanceof Promise) {
return value;
}
return new Promise(function(resolve) {
resolve(value);
});
};
Promise.reject = function(value) {
return new Promise(function(resolve, reject) {
reject(value);
});
};
Promise.race = function(promises) {
return new Promise(function(resolve, reject) {
for (var i = 0, len = promises.length; i < len; i++) {
promises[i].then(resolve, reject);
}
});
};
Promise.all = function(promises){
if (!promises || typeof promises.length === 'undefined')
throw new TypeError('Promise.all should accepts an array');
let values = [];
let i = 0;
let length = promises.length;
function processData(index,data){
values[index] = data;
i++;
if(i == length){
resolve(arr);
};
};
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data);
},reject);
};
});
}
複製程式碼