v8是怎麼實現更快的await ?深入理解await 的運行機制
最近v8
團隊發表一篇部落格Faster async functions and promises ,預計在v7.2
版本實現更快的異步函數和promise
。
文章內容看起來不是很容易理解,背後的原理比較隱蔽,不過部落格提到的一些ECMAScript標准文檔中的操作、任務,實際上都有已經實現的built-in api
,因此我們可以藉助我們比較熟悉的語法、api 來理解其中的原理,
也許本文有些說法不夠準確,歡迎糾正
Example
首先看下部落格開篇提到的代碼:
constp=Promise.resolve();(async()=>{awaitp;console.log("after:await");})();p.then(()=>{console.log("tick:a");}).then(()=>{console.log("tick:b");});
以
node v10
的執行結果為準,node v8
的實現是不符合ECMAScript標準
優秀的程序員總是能以簡單的例子解釋複雜的原理。代碼很簡單,但是執行結果可能出乎很多人意料:
tick:a tick:b after:await
如果你已經猜對了,本文的關鍵內容你已經掌握,不用往下看了:)。
為什麼after:await
會出現在tick:a
之後,甚至是tick:b
之後?要理解其中的原理,我們可以做一個小實驗。
將await 翻譯成promise
v8
部落格中是以偽代碼的方式解釋await
的執行邏輯:

我們可以用promise
語法寫成:
functionfoo2(v){constimplicit_promise=newPromise(resolve=>{constpromise=newPromise(res=>res(v));promise.then(w=>resolve(w));});returnimplicit_promise;}
按照同樣的方式,可以將文章開頭的代碼轉換成:
constp=Promise.resolve();(()=>{constimplicit_promise=newPromise(resolve=>{constpromise=newPromise(res=>res(p));promise.then(()=>{console.log("after:await");resolve();});});returnimplicit_promise;})();p.then(()=>{console.log("tick:a");}).then(()=>{console.log("tick:b");});
經過一些瑣碎的調試,發現問題真正的關鍵代碼是這一句: const promise = new Promise(res => res(p));
Resolved with another promise
了解Node.js或瀏覽器的事件循環的童鞋都知道,resolved promise的回調函數(reaction)是放在一個單獨的隊列MicroTask Queue
中。這個隊列會在事件循環的階段結束的時候被執行,只有當這個隊列被清空後,才能進入事件循環的下一個階段。
我們知道一個promise的.then
回調的返回值可以是一個任意值,也可以是另外一個promise。但是後者的處理邏輯可能有點反直覺。
在深入之前, 我們簡單說一下promise的幾種狀態:
- 我們說一個promise是resolved的,表示它不能被再次fulfill或reject,要么是被fulfill,要么被reject(這兩種情況,promise均有一個確定的non-promise result),要么遵循另外一個promise(隨之fulfill或reject)
- 我們說一個promise 是unresolved 的,表示它尚未被resolve
當一個promise(假設叫promiseA
。方便引用)被resolve,並且去遵循另外一個promise(叫p
)時,執行邏輯和前面兩種resolve情況非常不同,用偽代碼表示則是:
addToMicroTaskQueue (() => { // 任务A // 使用.then 方法,将promiseA 的状态和p 绑定p . then ( resolvePromiseA , // 任务B rejectPromiseA ); });
我們一步一步來分析:
- 首先,我們在
MicroTask Queue
添加任務A,該任務在ECMAScript標準中被定義為PromiseResolveThenableJob - 任務A,主要目的是使promiseA 遵循p 的狀態,將兩者的狀態關聯起來。
- 由於我們例子中p已經是resolved(狀態為fulfilled)的,所以立即將
resolvePromiseA
任務B添加到MicroTask Queue
中 - 在resolvePromiseA 執行後,promiseA 才是resolved (狀態為fulfilled,值為p 的fulfilled value)
我們可以看到,從new Promise(res=>res(p))
到該調用返回的promise真正被resolve至少需要兩次microtick
——在我們的例子中,是遍歷了兩次MicroTask Queue
這個時候,我們終於可以理清楚開頭代碼的執行順序:
01月28日更新,之前微任務隊列裡面的任務沒有考慮順序,這裡做一下修改,以下隊列裡的任務是順序有關,從左往右,左邊的先執行
1、當代碼執行完後
MicroTask Queue
有兩個任務:PromiseResolveThenableJob
,tick:a
2、開始執行runMicrotasks()
MicroTask Queue
變成:resolvePromiseA
,tick:b
- console: tick:a
3、 MicroTask Queue
沒有清空,繼續執行隊列中的任務
MicroTask Queue
變成:after:await
- console: tick:a, tick:b
4、繼續執行,清空MicroTaak Queue
- console: tick:a, tick:b, after:await
未來更快的v8
借助我們更熟悉的promise
,我們基本知道了現階段的await
的執行機制,這樣我們就能很好理解為什麼v8部落格中提到的改進可以使await
執行更快:
將new Promise(res=>res(p))
替換成Promise.resolve(p)
根據MDN文檔,當p
是一個promise時, Promise.resolve(p)
直接返回p
,而這是大概率事件。
因此,我們減少了promise之間狀態同步需要的兩次microtick
,那樣,上述代碼的輸出結果就是:
after:await tick:a tick:b
2019.02.12 更新
之前提到Node.js 8 的實現不符合標準,其實是V8 6.2 引入的一個bug
constpromise=newPromise(res=>res(p))
某些情況下(如p已經resolved)V8沒有嚴格按照promise-resolve-functions的第13步執行。
文章開頭的例子代碼,雖然優化後的執行結果和V8 6.2 一樣,但是背後的邏輯是不一樣的,附上對比圖:
