v8是怎麼實現更快的await ?深入理解await 的運行機制

blank

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的執行邏輯:

blank
原圖https://v8.dev/_img/fast-async/await-under-the-hood.svg

我們可以用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 ); });

我們一步一步來分析:

  1. 首先,我們在MicroTask Queue添加任務A,該任務在ECMAScript標準中被定義為PromiseResolveThenableJob
  2. 任務A,主要目的是使promiseA 遵循p 的狀態,將兩者的狀態關聯起來。
  3. 由於我們例子中p已經是resolved(狀態為fulfilled)的,所以立即將resolvePromiseA任務B添加到MicroTask Queue
  4. 在resolvePromiseA 執行後,promiseA 才是resolved (狀態為fulfilled,值為p 的fulfilled value)

我們可以看到,從new Promise(res=>res(p))到該調用返回的promise真正被resolve至少需要兩次microtick ——在我們的例子中,是遍歷了兩次MicroTask Queue

這個時候,我們終於可以理清楚開頭代碼的執行順序:

01月28日更新,之前微任務隊列裡面的任務沒有考慮順序,這裡做一下修改,以下隊列裡的任務是順序有關,從左往右,左邊的先執行

1、當代碼執行完後

    • MicroTask Queue有兩個任務: PromiseResolveThenableJobtick:a

2、開始執行runMicrotasks()

    • MicroTask Queue變成: resolvePromiseAtick: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 一樣,但是背後的邏輯是不一樣的,附上對比圖:

blank
await 在V8 不同版本上的差異

What do you think?

Written by marketer

blank

JavaScript 算法之複雜度分析

blank

關於高效、高質和高產