前言
在上一篇《Promise 原始碼:實現一個簡單的 Promise》當中,我們實現了一個可以簡單可用的 Promise。但它實際上還是有不少的缺陷的,比如:
- Promise 建構函式裡直接同步 resolve,則執行不到 then。
- 只有 resolve,沒有 reject。
- 一些極端的情況沒有考慮到。
接下來通過閱讀 github 上 then/promise 專案原始碼來學習學習 Promise 的實現原理。
本篇主要解決第一個問題,即同步執行 resolve,也是上篇實現的程式碼中忽略的問題:在執行 resolve 之後,沒能執行到 then 的回撥函式。
new Promise(function (resolve) {
resolve(1);
}).then(function (val) {
console.log(val);
});
複製程式碼
注:本次閱讀的是 then/promise 的 3.0.0 版本,原始碼請戳 這裡。
解讀
then/promise 專案中 Promise 的程式碼實現只在 index.js 檔案,而且不到 100 行程式碼,閱讀起來也沒那麼的困難。
nextTick
首先熱身一下,先來看看這一端程式碼:
var nextTick
if (typeof setImmediate === `function`) { // IE >= 10 & node.js >= 0.10
nextTick = function(fn){ setImmediate(fn) }
} else if (typeof process !== `undefined` && process && typeof process.nextTick === `function`) { // node.js before 0.10
nextTick = function(fn){ process.nextTick(fn) }
} else {
nextTick = function(fn){ setTimeout(fn, 0) }
}
複製程式碼
這一段程式碼跟 js 的事件迴圈有關,在執行上下文棧為空時,才會去執行以上的任務佇列。這裡做判斷主要是相容 node 和瀏覽器,可以把以上程式碼就當成 setTimeout。
Promise
熱身完畢,接下來看的是 Promise 的實現。首先程式碼寫的一個建構函式,所有的程式碼實現都在其建構函式裡,然後暴露出一個 then 函式。
function Promise(fn) {
// ...
then.then = function() {
// ...
}
// ...
}
複製程式碼
建構函式裡定義了許許多多的變數和函式,暫時不一一解釋,用到的時候再解釋。
我們先來看看同步與非同步的兩種執行步驟:
// 同步
new Promise(function (resolve) {
resolve(1);
}).then(function (val) {
console.log(val);
});
// 非同步
new Promise(function (resolve) {
setTimeout(function () {
resolve(1);
}, 1000);
}.then(function (val) {
console.log(val);
});
複製程式碼
同步和非同步的時候,函式的執行順序是不一樣的。
constructor -> fn --同步--> resolve(reject) -> then -> then 回撥
constructor -> fn --非同步--> then -> resolve(reject) -> then 回撥
複製程式碼
同步
先從同步的使用方式入手,它會先執行 resolve,接著執行 then 函式,最後執行 then 回撥函式。
new Promise(function (resolve) {
resolve(1);
}).then(function (val) {
console.log(val);
});
複製程式碼
從 fn 的執行開始看吧,fn 執行失敗會 catch 呼叫 reject:
try { fn(resolve, reject) }
catch(e) { reject(e) }
複製程式碼
resolve
然後開始執行 resolve 函式,將 1 作為引數傳遞。來看看 resolve 的實現,先把一些暫時不用到的程式碼去掉:
function resolve(newValue) {
resolve_(newValue)
}
function resolve_(newValue) {
if (state !== null)
return
try {
state = true
value = newValue
finale()
} catch (e) { reject_(e) }
}
複製程式碼
這裡用到了 state 和 value 兩個變數,還呼叫了一個 finale 函式。
state 是用來記住狀態的一個變數,執行 resolve 成功賦值 true,執行 reject 成功賦值 false,否則為 null。該變數只要是用來防止多次 resolve,只要呼叫程式碼呼叫了 resolve,後面的 resolve 一律無效。
value 用來儲存 resolve 執行時傳入的引數,以便後面 then 的回撥時能取到。
finale 暫時忽略,因為同步執行的時候,finale 裡的程式碼幾乎是不執行的。
then
執行完 resolve 函式,接著執行 then 函式:
this.then = function(onFulfilled, onRejected) {
return new Promise(function(resolve, reject) {
handle({ onFulfilled: onFulfilled, onRejected: onRejected, resolve: resolve, reject: reject })
})
}
複製程式碼
先暫時不管返回了 Promise 例項,這個是為了後面的 then 鏈式呼叫。這裡 then 函式執行後,會將 then 的回撥函式 onFulfilled 和 onRejected 作為引數傳入 handle 函式之中。onFulfilled 是 resolve 成功後的回撥,onRejected 是 reject 成功後的回撥。
handle
跳到 handle 函式:
function handle(deferred) {
nextTick(function() {
var cb = state ? deferred.onFulfilled : deferred.onRejected
if (typeof cb !== `function`){
(state ? deferred.resolve : deferred.reject)(value)
return
}
var ret
try {
ret = cb(value)
}
catch (e) {
deferred.reject(e)
return
}
deferred.resolve(ret)
})
}
複製程式碼
handle 函式最主要的就是執行 cb 的程式碼。其它的程式碼都是在做判斷,判斷 cb 的類似是否是函式。所以 handle 函式在此時就是執行 then 回撥函式,將之前 resolve 存的 value 作為引數傳遞。
這裡的 nextTick 幾乎是無效的,因為程式碼是同步執行的,它會在非同步的時候發揮作用的。最後執行的 deferred.resolve(ret)
也是為了實現 then 連結呼叫,在此時執行與執行沒多大的區別。
到這裡,Promise 的同步就執行完畢了,再次回顧一下執行順序:
constructor -> fn --同步--> resolve(reject) -> then -> then 回撥
複製程式碼
總結
古往今來同步執行的程式碼都比較好理解,畢竟是按順序執行的。再看一遍 Promise 執行同步程式碼:
new Promise(function (resolve) {
resolve(1);
}).then(function (val) {
console.log(val);
});
複製程式碼
執行同步程式碼時,會先執行 resolve,會用變數 value 來儲存傳遞的引數,再用 statue 變數來儲存狀態,第一是防止多次 resolve,第二是通過它來判斷回撥 onFulfilled(成功回撥) 還是 onRejected(失敗回撥)。
執行的順序如下:
constructor -> fn --同步--> resolve(reject) -> then -> then 回撥
複製程式碼
同時,Promise 的實現程式碼裡用到了許多的 try catch,一旦報錯 catch 到,就會執行 reject 而不是 resolve,所以 Promise 一般不會指令碼報錯,而是回撥 reject 函式,這點是需要注意的。