以前在網上看到一道Promise執行順序的題目——打印如下程序的輸出:javascript
new Promise(resolve => {
console.log(1);
resolve(3);
}).then(num => {
console.log(num)
});
console.log(2)複製代碼
這道題的輸出是123,爲何不是132呢?由於我一直理解Promise是沒有異步功能,它只是幫忙解決異步回調的問題,實質上是和回調是同樣的,因此若是按照這個想法,resolve以後應該會馬上then。但實際上並非。難道用了setTimeout?html
若是在promise裏面再加一個promise:java
new Promise(resolve => {
console.log(1);
resolve(3);
Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2)複製代碼
執行順序是1243,第二個Promise的順序會比第一個的早,因此直觀來看也是比較奇怪,這是爲何呢?node
Promise的實現有不少庫,有jQuery的deferred,還有不少提供polyfill的,如es6-promise,lie等,它們的實現都基於Promise/A+標準,這也是ES6的Promise採用的。git
爲了回答上面題目的執行順序問題,必須得理解Promise是怎麼實現的,因此得看那些庫是怎麼實現的,特別是我錯誤地認爲不存在的Promise的異步是怎麼實現的,由於最後一行的console.log(2)它並非最後執行的,那麼一定有某些相似於setTimeout的異步機制讓上面同步的代碼在異步執行,因此它才能在代碼執行完了以後才執行。es6
固然咱們不僅是爲了解答一道題,主要仍是藉此瞭解Promise的內部機制。讀者若是有時間有興趣能夠自行分析,而後再回過頭來比較一下本文的分析。或者你能夠跟着下面的思路,操起鼠標和鍵盤和我一塊兒幹。github
這裏使用lie的庫,相對於es6-promise來講代碼更容易看懂,先npm install一下:npm
npm install lie複製代碼
讓代碼在瀏覽器端運行,準備如下html:數組
<!DOCType html> <html> <head> <meta charset="utf-8"> </head> <body> <script src="node_modules/lie/dist/lie.js"></script> <script src="index.js"></script> </body> </html>複製代碼
其中index.js的內容爲:promise
console.log(Promise);
new Promise(resolve => {
console.log(1);
resolve(3);
Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2);複製代碼
把Promise打印一下,確認已經把原生的那個覆蓋了,對好比下:
由於原生的Promise咱們是打不了斷點的,因此才須要藉助一個第三方的庫。
咱們在第4行的resolve(3)那裏打個斷點進去看一下resolve是怎麼執行的,層層進去,最後的函數是這個:
咱們發現,這個函數好像沒幹啥,它就是設置了下self的state狀態爲FULFILLED(完成),而且把結果outcome設置爲調resolve傳進來的值,這裏是3,若是resolve傳來是一個Promise的話就會進入到上圖187行的Promise鏈處理,這裏咱們不考慮這種狀況。這裏的self是指向一個Promise對象:
它主要有3個屬性——outcome、queue、state,其中outcome是resolve傳進來的結果,state是Promise的狀態,在第83行的代碼能夠查到Promise的狀態總共有3種:
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];複製代碼
Rejected失敗,fulfilled成功,pending還在處理中,在緊接着89行的Promise的構造函數能夠看到,state初始化的狀態爲pending:
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}複製代碼
而且在右邊的調用棧能夠看到,resolver是由Promise的構造函數觸發執行的,即當你new Promise的時候就會執行傳參的函數,以下圖所示:
傳進來的函數支持兩個參數,分別是resolve和reject回調:
let resolver = function(resolve, reject) {
if (success) resolve();
else reject();
};
new Promise(resolver);複製代碼
這兩個函數是Promise內部定義,可是要在你的函數裏調一下它的函數,告訴它何時成功了,何時失敗了,這樣它才能繼續下一步的操做。因此這兩個函數參數是傳進來的,它們是Promise的回調函數。Promise是怎麼定義和傳遞這兩個函數的呢?仍是在剛剛那個斷點的位置,可是咱們改變一下右邊調用棧顯示的位置:
上圖執行的thenable函數就是咱們傳給它的resolver,而後傳遞onSuccess和onError,分別是咱們在resolver裏面寫的resolve和reject這兩個參數。若是咱們調了它的resolve即onSuccess函數,它就會調236行的handlers.resolve就到了咱們第一次打斷點的那張圖,這裏再放一次:
而後去設置當前Promise對象的state,outcome等屬性。這裏沒有進入到193行的while循環裏,由於queue是空的。這個地方下文會繼續提到。
接着,咱們在then那裏打個斷點進去看一下:
then又作了些什麼工做呢?以下圖所示:
then能夠傳兩個參數,分別爲成功回調和失敗回調。咱們給它傳了一個成功回調,即上圖劃線的地方。而且因爲在resolver裏面已經把state置成fulfilled完成態了,因此它會執行unwrap函數,並傳遞成功回調、以及resolve給的結果outcome(還有一個參數promise,主要是用於返回,造成then鏈)。
unwrap函數是這樣實現的:
在167行執行then裏傳給Promise的成功回調,並傳遞結果outcome。
這段代碼是包在一個immediate函數裏的,這裏就是解決Promise異步問題的關鍵了。而且咱們在node_modules目錄裏面,也發現了lie使用了immediate庫,它能夠實現一個nextTick的功能,即在當前代碼邏輯單元同步執行完了以後馬上執行,至關於setTimeout 0,可是它又不是直接用setTimeout 0實現的。
咱們重點來看一下它是怎麼實現一個nextTick的功能的。immediate裏面會調一個scheduleDrain(drain是排水的意思):
function immediate(task) {
// 這個判斷先忽略
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}複製代碼
實現邏輯在這個scheduleDrain,它是這麼實現的:
var Mutation = global.MutationObserver || global.WebKitMutationObserver;
var scheduleDrain = null;
{
// 瀏覽器環境,IE11以上支持
if (Mutation) {
// ...
}
// Node.js環境
else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined')
}
// 低瀏覽器版本解決方案
else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) {
}
// 最後實在沒辦法了,用最次的setTimeout
else {
scheduleDrain = function () {
setTimeout(nextTick, 0);
};
}
}複製代碼
它會有一個兼容性判斷,優先使用MutationObserver,而後是使用script標籤的方式,這種到IE6都支持,最後啥都不行就用setTimeout 0.
咱們主要看一下Mutation的方式是怎麼實現的,MDN上有介紹這個MutationObserver的用法,能夠用它來監聽DOM結點的變化,如增刪、屬性變化等。Immediate是這麼實現的:
if (Mutation) {
var called = 0;
var observer = new Mutation(nextTick);
var element = global.document.createTextNode('');
// 監聽節點的data屬性的變化
observer.observe(element, {
characterData: true
});
scheduleDrain = function () {
// 讓data屬性發生變化,在0/1之間不斷切換,
// 進而觸發observer執行nextTick函數
element.data = (called = ++called % 2);
};
}複製代碼
使用nextTick回調註冊一個observer觀察者,而後建立一個DOM節點element,成爲observer的觀察對象,觀察它的data屬性。當須要執行nextTick函數的時候,就調一下scheduleDrain改變data屬性,就會觸發觀察者的回調nextTick。它是異步執行的,在當前代碼單元執行完以後馬上之行,但又是在setTimeout 0以前執行的,也就是說,如下代碼,第一行的5是最後輸出的:
setTimeout(()=> console.log(5), 0);
new Promise(resolve => {
console.log(1);
resolve(3);
// Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2);複製代碼
這個時候,咱們就能夠回答爲何上面代碼的輸出順序是123,而不是132了。第一點能夠確定的是1是最早輸出的,由於new一個Promise以後,傳給它的resolver同步執行,因此1最早打印。執行了resolve(3)以後,就會把當前Promiser對象的state改爲完成態,並記錄結果outcome。而後跳出來執行then,把傳給then的成功回調給immediate在nextTick執行,而nextTick是使用Mutation異步執行的,因此3會在2以後輸出。
若是在promise裏面再寫一個promsie的話,因爲裏面的promise的then要比外面的promise的then先執行,也就是說它的nextTick更先註冊,因此4是在3以前輸出。
這樣基本上就解釋了Promise的執行順序的問題。可是咱們還沒說它的nextTick是怎麼實現的,上面代碼在執行immediate的時候把成功回調push到一個全局的數組queue裏面,而nextTick是把這些回調按順序執行,以下代碼所示:
function nextTick() {
draining = true;
var i, oldQueue;
var len = queue.length;
while (len) {
oldQueue = queue;
// 把queue清空
queue = [];
i = -1;
// 執行當前全部回調
while (++i < len) {
oldQueue[i]();
}
len = queue.length;
}
draining = false;
}複製代碼
它會先把排水的變量draining設置成true,而後處理完成以後再設置成false,咱們再回顧一下剛剛執行immediate的判斷:
function immediate(task) {
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}複製代碼
因爲JS是單線程的,因此我以爲這個draining的變量判斷好像沒有太大的必要。另一個判斷,當queue爲空時,push一個變量進來,這個時候queue只有1個元素,返回值就爲1。因此若是以前已經push過了,那麼這裏就不用再觸發nextTick,由於第一次的push會把全部queue回調元素都執行的,只要保證後面的操做有被push到這個queue裏面就行了。因此這個判斷是一個優化。
另外,es6-promise的核心代碼是同樣的,只是它把immediate函數改爲asap(as soon as possible),它也是優先使用Mutation.
還有一個問題,上面說的resolver的代碼是同步,可是咱們常常用Promise是用在異步的狀況,resolve是異步調的,不是像上面同步調的,如:
let resolver = function(resolve) {
setTimeout(() => {
// 異步調用resolve
resolve();
}, 2000);
// resolver執行完了還沒執行resolve
};
new Promise(resolver).then(num => console.log(num));複製代碼
這個時候,同步執行完resolver,但還沒執行resolve,因此在執行then的時候這個Promise的state仍是pending的,就會走到134的代碼(剛剛執行的是132行的unwrap):
它會建立一個QueueItem而後放到當前Promise對象的queue屬性裏面(注意這裏的queue和上面說的immediate裏全局的queue是兩個不一樣的變量)。而後異步執行結束調用resolve,這個時候queue不爲空了:
就會執行queue隊列裏面的成功回調。由於then是能夠then屢次的,因此成功回調可能會有多個。它也是調用immediate,在nextTick的時候執行的。
也就是說若是是同步resolve的,是經過MutationObserver/Setimeout 0之類的方式在當前的代碼單元執行完以後馬上執行成功回調;而若是是異步resolve的,是先把成功回調放到當前Promise對象的一個隊列裏面,等到異步結束了執行resolve的時候再用一樣的方式在nextTick調用成功回調。
咱們還沒說失敗的回調,但大致是類似的。