早前有針對 Promise
的語法寫過博文,不過僅限入門級別,淺嘗輒止食而無味。後面一直想寫 Promise
實現,礙於理解程度有限,屢次下筆未能滿意。一拖再拖,時至今日。javascript
隨着 Promise/A+規範、ECMAscript規範 對 Promise
API 制定執行落地,Javascript 異步操做的基本單位也逐漸從 callback
轉換到 promise
。絕大多數JavaScript/DOM
平臺新增的異步API(Fetch
、Service worker
)也都是基於Promise
構建的。這其中對 Promise
理解不是僅看過 API,讀過幾篇實踐就能徹底掌握的。筆者以此行文,剖析細節,伴隨讀者一塊兒成長,砥礪前行。html
本文爲前端異步編程解決方案實踐系列第二篇,主要分析 Promise
內部機制及實現原理。後續異步系列還會包括Generator
、Async/Await
相關,挖坑佔位。前端
注:本文 Promise
遵照 Promises/A+ 規範,實現參照 then/promise。java
既然要講實現原理,難免要承前啓後交代清楚 Promise
是什麼。查閱文檔,以下:git
A promise represents the eventual result of an asynchronous operation. --Promises/A+github
A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. --ECMAscriptweb
Promises/A+
規範中表示爲一個異步操做的最終結果,ECMAscript
規範定義爲延時或異步計算最終結果的佔位符。言簡意賅,但稍微聱牙詰屈,如何表述更淺顯易懂呢?編程
說個故事,Promise
是一個美好的承諾,承諾自己會作出正確延時或異步操做。承諾會解決callback
處理異步回調可能產生的調用過早,調用過晚、調用次數過多過少、吞掉可能出現的錯誤或異常問題等。另外承諾只接受首次 resolve(..)
或 reject(..)
決議,承諾自己狀態轉變後不會再變,承諾全部經過 then(..)
註冊的回調老是依次異步調用,承諾全部異常總會被捕獲拋出。她,是一個可信任的承諾。api
嚴謹來說,Promise
是一種封裝和組合將來值得易於複用機制,實現關注點分離、異步流程控制、異常冒泡、串行/並行控制等。數組
注:文中說起 callback
問題詳情見<<你不知道的JavaScript(中卷)>> 2.3 、3.3章節
Promise A+
規範字數很少簡明扼要,但仔細翻讀,其中仍有有幾點須要引人注意。
thenable
是一個定義 then(..)
方法的對象或函數。thenable
對象的存在目的是使 Promise
的實現更具備通用性,只要其暴露出一個遵循 Promise/A+
規範的 then(..)
方法。同時也會使遵循 Promise/A+
規範的實現能夠與那些不太規範但可用的實現能良好共存。
識別 thenable
或行爲相似 Promise
對象能夠根據其是否具備 then(..)
方法來判斷,這其實叫類型檢查也可叫鴨式辯型(duck typing
)。對於 thenable
值鴨式類型檢測大體相似於:
if ( p !== null &&
(
typeof p === 'object' ||
typeof p === 'function'
) &&
typeof p.then === 'function'
) {
// thenable
} else {
// 非 thenable
}
複製代碼
衆所周知,Promise
實例化時傳入的函數會當即執行,then(...)
中的回調須要異步延遲調用。至於爲何要延遲調用,後文會慢慢解讀。這裏有個重要知識點,回調函數異步調用時機。
onFulfilled or onRejected must not be called until the execution context stack contains only platform code --Promise/A+
簡譯爲onFulfilled
或 onRejected
只在執行環境堆棧僅包含平臺代碼時纔可被調用。稍有疑惑,Promise/A+ 規範又對此句加以解釋:「實踐中要確保 onFulfilled
和 onRejected
方法異步執行,且應該在 then
方法被調用的那一輪事件循環以後的新執行棧中執行。這個事件隊列能夠採用宏任務 macro-task機制或微任務 micro-task機制來實現。」
雖然Promise A+
未明確指出是以 microtask
仍是 macrotask
形式放入隊列,但 ECMAScript
規範明確指出 Promise
必須以 Promise Job 形式加入 job queues
(也就是 microtask)。Job Queue 是 ES6 中新提出的概念,創建在事件循環隊列之上。job queue
存在也是爲了知足一些低延遲的異步操做。
敲黑板劃重點,注意這裏 macrotask
microtask
分別表示異步任務的兩種分類。在掛起任務時,JS 引擎會將全部任務按照類別分到兩個隊列中,首先在 macrotask
的隊列(也叫 task queue
)中取出第一個任務,執行完畢後取出 microtask 隊列中的全部任務順序執行;以後再取 macrotask 任務,周而復始,直至兩個隊列的任務都取完。
對於microtask
執行時機,whatwg HTML規範中也有闡述,詳情可點擊查閱。更多相關文章可參考附錄 event loop
。
再看一個示例,加深理解:
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
複製代碼
打印的順序?正確答案是:promise1, promise2, setTimeout
。
在進一步實現 Promise
對象以前,簡單模擬異步執行函數供後文Promise
回調使用(也可採用 asap庫等)。
var asyncFn = function () {
if (typeof process === 'object' && process !== null &&
typeof(process.nextTick) === 'function'
) {
return process.nextTick;
} else if (typeof(setImmediate) === 'function') {
return setImmediate;
}
return setTimeout;
}();
複製代碼
Promise
必須爲如下三種狀態之一:等待態(Pending
)、執行態(Fulfilled
)和拒絕態(Rejected
)。一旦Promise
被resolve
或reject
,不能再遷移至其餘任何狀態(即狀態 immutable
)。
爲保持代碼清晰,暫無異常處理。同時爲表述方便,約定以下:
從構造函數開始,咱們一步步實現符合 Promsie A+
規範的 Promise
。大概描述下,Promise
構造函數須要作什麼事情。
Promise
狀態(pending
)then(..)
註冊回調處理數組(then
方法可被同一個 promise
調用屢次)fn
函數,傳入Promise
內部 resolve
、reject
函數function Promise (fn) {
// 省略非 new 實例化方式處理
// 省略 fn 非函數異常處理
// promise 狀態變量
// 0 - pending
// 1 - resolved
// 2 - rejected
this._state = 0;
// promise 執行結果
this._value = null;
// then(..) 註冊回調處理數組
this._deferreds = [];
// 當即執行 fn 函數
try {
fn(value => {
resolve(this, value);
},reason => {
reject(this, reason);
})
} catch (err) {
// 處理執行 fn 異常
reject(this, err);
}
}
複製代碼
_state
和 _value
變量很容易理解,_deferreds
變量作什麼?規範描述:then
方法能夠被同一個 promise
調用屢次。爲知足屢次調用 then
註冊回調處理,內部選擇使用 _deferreds
數組存儲處理對象。具體處理對象結構,見 then
函數章節。
最後執行 fn
函數,並調用 promise
內部的私有方法 resolve
和 reject
。resolve
和 reject
內部細節隨後介紹。
Promise A+
提到規範專一於提供通用的 then
方法。then
方法能夠被同一個 promise
調用屢次,每次返回新 promise
對象 。then
方法接受兩個參數onResolved
、onRejected
(可選)。在 promise
被 resolve
或 reject
後,全部 onResolved
或 onRejected
函數須按照其註冊順序依次回調,且調用次數不超過一次。
根據上述,then
函數執行流程大體爲:
promise
對象用來返回(保持then
鏈式調用)then(..)
註冊回調處理函數結構體promise
狀態,pending
狀態存儲延遲處理對象 deferred
,非pending
狀態執行 onResolved
或 onRejected
回調Promise.prototype.then = function (onResolved, onRejected) {
var res = new Promise(function () {});
// 使用 onResolved,onRejected 實例化處理對象 Handler
var deferred = new Handler(onResolved, onRejected, res);
// 當前狀態爲 pendding,存儲延遲處理對象
if (this._state === 0) {
this._deferreds.push(deferred);
return res;
}
// 當前 promise 狀態不爲 pending
// 調用 handleResolved 執行onResolved或onRejected回調
handleResolved(this, deferred);
// 返回新 promise 對象,維持鏈式調用
return res;
};
複製代碼
Handler
函數封裝存儲 onResolved
、onRejected
函數和新生成 promise
對象。
function Handler (onResolved, onRejected, promise) {
this.onResolved = typeof onResolved === 'function' ? onResolved : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
複製代碼
鏈式調用爲何要返回新的 promise
如咱們理解,爲保證 then
函數鏈式調用,then
須要返回 promise
實例。但爲何返回新的 promise
,而不直接返回 this
當前對象呢?看下面示例代碼:
var promise2 = promise1.then(function (value) {
return Promise.reject(3)
})
複製代碼
假如 then
函數執行返回 this
調用對象自己,那麼 promise2 === promise1
,promise2
狀態也應該等於 promise1
同爲 resolved
。而 onResolved
回調中返回狀態爲 rejected
對象。考慮到 Promise
狀態一旦 resolved
或 rejected
就不能再遷移,因此這裏 promise2
也沒辦法轉爲回調函數返回的 rejected
狀態,產生矛盾。
handleResolved
函數功能爲根據當前 promise
狀態,異步執行 onResolved
或 onRejected
回調函數。因在 resolve
或 reject
函數內部一樣須要相關功能,提取爲單獨模塊。往下翻閱查看。
Promise
實例化時當即執行傳入的 fn
函數,同時傳遞內部 resolve
函數做爲參數用來改變 promise
狀態。resolve
函數簡易版邏輯大概爲:判斷並改變當前 promise
狀態,存儲 resolve(..)
的 value
值。判斷當前是否存在 then(..)
註冊回調執行函數,若存在則依次異步執行 onResolved
回調。
但如文初所 thenable
章節描述,爲使 Promise
的實現更具備通用性,當 value
爲存在 then(..)
方法的 thenable
對象,須要作 Promise Resolution Procedure
處理,規範描述爲 [[Resolve]](promise, x)
。(x
即 爲後面 value
參數)。
具體處理邏輯流程以下:
若是 promise
和 x
指向同一對象,以 TypeError
爲據因拒絕執行 promise
若是 x
爲 Promise
,則使 promise
接受 x
的狀態
若是 x
爲對象或函數
x.then
賦值給 then
x.then
的值時拋出錯誤 e ,則以 e 爲據因拒絕 promise
then
是函數,將 x
做爲函數的做用域 this 調用之。x
不爲對象或者函數,以 x
爲參數執行 promise
原文參考Promise A+
規範 Promise Resolution Procedure 。
function resolve (promise, value) {
// 非 pending 狀態不可變
if (promise._state !== 0) return;
// promise 和 value 指向同一對象
// 對應 Promise A+ 規範 2.3.1
if (value === promise) {
return reject( promise, new TypeError('A promise cannot be resolved with itself.') );
}
// 若是 value 爲 Promise,則使 promise 接受 value 的狀態
// 對應 Promise A+ 規範 2.3.2
if (value && value instanceof Promise && value.then === promise.then) {
var deferreds = promise._deferreds
if (value._state === 0) {
// value 爲 pending 狀態
// 將 promise._deferreds 傳遞 value._deferreds
// 偷個懶,使用 ES6 展開運算符
// 對應 Promise A+ 規範 2.3.2.1
value._deferreds.push(...deferreds)
} else if (deferreds.length !== 0) {
// value 爲 非pending 狀態
// 使用 value 做爲當前 promise,執行 then 註冊回調處理
// 對應 Promise A+ 規範 2.3.2.二、2.3.2.3
for (var i = 0; i < deferreds.length; i++) {
handleResolved(value, deferreds[i]);
}
// 清空 then 註冊回調處理數組
value._deferreds = [];
}
return;
}
// value 是對象或函數
// 對應 Promise A+ 規範 2.3.3
if (value && (typeof value === 'object' || typeof value === 'function')) {
try {
// 對應 Promise A+ 規範 2.3.3.1
var then = obj.then;
} catch (err) {
// 對應 Promise A+ 規範 2.3.3.2
return reject(promise, err);
}
// 若是 then 是函數,將 value 做爲函數的做用域 this 調用之
// 對應 Promise A+ 規範 2.3.3.3
if (typeof then === 'function') {
try {
// 執行 then 函數
then.call(value, function (value) {
resolve(promise, value);
}, function (reason) {
reject(promise, reason);
})
} catch (err) {
reject(promise, err);
}
return;
}
}
// 改變 promise 內部狀態爲 `resolved`
// 對應 Promise A+ 規範 2.3.3.四、2.3.4
promise._state = 1;
promise._value = value;
// promise 存在 then 註冊回調函數
if (promise._deferreds.length !== 0) {
for (var i = 0; i < promise._deferreds.length; i++) {
handleResolved(promise, promise._deferreds[i]);
}
// 清空 then 註冊回調處理數組
promise._deferreds = [];
}
}
複製代碼
resolve
函數邏輯較爲複雜,主要集中在處理 value
(x
)值多種可能性。若是 value
爲 Promise
且狀態爲pending
時,須使 promise
接受 value
的狀態。在 value
狀態爲 pending
時,簡單將 promise
的 deferreds
回調處理數組賦予 value
deferreds
變量。非 pending
狀態,使用 value
內部值回調 promise
註冊的 deferreds
。
若是 value
爲 thenable
對象,以 value
做爲函數的做用域 this
調用之,同時回調調用內部 resolve(..)
、reject(..)
函數。
其餘情形則以 value
爲參數執行 promise
,調用 onResolved
或 onRejected
處理函數。
事實上,Promise A+規範
定義的 Promise Resolution Procedure
處理流程是用來處理 then(..)
註冊的 onResolved
或 onRejected
調用返回值 與 then
新生成 promise
之間關係。不過考慮到 fn
函數內部調用 resolve(..)
產生值 與當前 promise
值仍然存在相同關係,邏輯一致,寫進相同模塊。
Promise
內部私有方法 reject
相較於 resolve
邏輯簡單不少。以下所示:
function reject (promise, reason) {
// 非 pending 狀態不可變
if (promise._state !== 0) return;
// 改變 promise 內部狀態爲 `rejected`
promise._state = 2;
promise._value = reason;
// 判斷是否存在 then(..) 註冊回調處理
if (promise._deferreds.length !== 0) {
// 異步執行回調函數
for (var i = 0; i < promise._deferreds.length; i++) {
handleResolved(promise, promise._deferreds[i]);
}
promise._deferreds = [];
}
}
複製代碼
瞭解完 Promise
構造函數、then
函數、以及內部 resolve
和 reject
函數實現,你會發現其中全部的回調執行咱們都統一調用 handleResolved
函數,那 handleResolved
到底作了哪些事情,實現又有什麼注意點?
handleResolved
函數具體會根據 promise
當前狀態判斷調用 onResolved
、onRejected
,處理 then(..)
註冊回調爲空情形,以及維護鏈式 then(..)
函數後續調用。具體實現以下:
function handleResolved (promise, deferred) {
// 異步執行註冊回調
asyncFn(function () {
var cb = promise._state === 1 ?
deferred.onResolved : deferred.onRejected;
// 傳遞註冊回調函數爲空狀況
if (cb === null) {
if (promise._state === 1) {
resolve(deferred.promise, promise._value);
} else {
reject(deferred.promise, promise._value);
}
return;
}
// 執行註冊回調操做
try {
var res = cb(promise._value);
} catch (err) {
reject(deferred.promise, err);
}
// 處理鏈式 then(..) 註冊處理函數調用
resolve(deferred.promise, res);
});
}
複製代碼
具體處理註冊回調函數 cb
爲空情形,以下面示例。判斷當前回調 cb
爲空時,使用 deferred.promise
做爲當前 promise
結合 value
調用後續處理函數繼續日後執行,實現值穿透空處理函數日後傳遞。
Promise.resolve(233)
.then()
.then(function (value) {
console.log(value)
})
複製代碼
關於 then
鏈式調用,簡單再說下。實現 then
函數的鏈式調用,只須要在 Promise.prototype.then(..)
處理函數中返回新的 promise
實例便可。但除此以外,還須要依次調用 then
註冊的回調處理函數。如 handleResolved
函數最後一句 resolve(deferred.promise, res)
所示。
then 註冊回調函數爲何異步執行
這裏回答開篇所提到的一個問題,then
註冊的 onResolved
、onRejected
函數爲何要採用異步執行?再來看一段實例代碼。
var a = 1;
promise1.then(function (value) {
a = 2;
})
console.log(a)
複製代碼
promise1 內部執行同步或異步操做未知。假如未規定 then
註冊回調爲異步執行,則這裏打印 a 可能存在兩種值。promise1 內部同步操時 a === 2,相反執行異步操做時 a === 1。爲屏蔽依賴外部的不肯定性,規範指定 onFulfilled
和 onRejected
方法異步執行。
若是 promise
被 rejected
,則會調用拒絕回調並傳入拒由。好比在 Promise
的建立過程當中(fn
執行時)出現異常,那這個異常會被捕捉並調用 onRejected
。
但還存在一處細節,若是 Promise
完成後調用 onResolved
查看結果時出現異常錯誤會怎麼樣呢?注意此時 onRejected
不會被觸發執行,由於 onResolved
內部異常並不會改變當前 promise
狀態(仍爲resolved
),而是改變 then
中返回新的 promise
狀態爲 rejected
。異常未丟失但也未調用錯誤處理函數。
如何處理?Ecmascript
規範有定義Promise.prototype.catch
方法,假如你對 onResolved
處理過程沒有信心或存在異常 case 狀況,最好仍是在 then
函數後調用 catch
方法作異常捕獲兜底處理。
查閱 Promise
相關文檔或書籍,你還會發現 Promise
相關有用的API:Promise.race
、Promise.all
、 Promise.resolve
、Promise.reject
。這裏對 Promise.race
方法實現作個展現,剩餘可自行參考實現。
Promise.race = function (values) {
return new Promise(function (resolve, reject) {
values.forEach(function(value) {
Promise.resolve(value).then(resolve, reject);
});
});
};
複製代碼
寫到這裏,核心的 Promise
實現也逐漸完成,Promise
內部細節也在文中或代碼中一一描述。限於筆者自己能力有限,對於 promise
內部實現暫未達到庖丁解牛程度,有些地方一筆帶過,可能讀者心生疑惑。針對不解的地方,建議多讀兩遍或參考書籍理解。
若是讀完拙文能多少有點收穫,也算達到筆者初衷,你們一塊兒成長。最後筆者也非完人,文中難免語句不順或詞不達意,望理解。若是對於本文有任何疑問或錯誤,歡迎斧正,在此先行謝過。
參考文檔
參考書籍
event loop
打個廣告,歡迎關注筆者公衆號