您當前的位置:首頁 > 書法

探討:當AsyncAwait的遇到了EventLoop

作者:由 c.zliang 發表于 書法時間:2019-10-17

ES的“非同步處理”發展到了今天,已經出現了相對成熟的方案了:Async/Await。在這份方案中,被關鍵詞:async修飾的函式將返回一個promise,並且可以被await呼叫。

那從EventLoop的角度去理解,async/await是一個怎樣的執行過程呢?

本文旨意:

1、探究Async函式轉化為普通函式的等價形式

2、探究

async/await

在EventLoop中的執行機制

申明:

1、如果特殊申明,本文中的程式碼均執行在Chrome v73以上版本(例如筆者當前安裝的版本:v77)

一、一道面試題

二、轉換對比

三、PromiseResolveThenableJob

四、案例分析

五、Await轉換

六、總結

本文案例涉及到的一些必備的知識點:

1、ES中涉及到Async/Await的非同步知識

2、Promise的相關知識點

3、瀏覽器中的EventLoop(事件迴圈)

4、Thenable物件與Promise,EventLoop中的Promise

5、new Promoise((rs, rj)=>{rs( otherPromise )})涉及到的非同步Job

希望讀者對前面4個要點有一定的瞭解甚至掌握,這樣才能更方便讀者閱讀本文

第5點涉及到的Job其實就是PromiseResolveThenableJob,會在文末提供更詳細的文件資料

文章內容難度:★★★

一、一道面試題

為了方便引用,我們把它命名為:

【1.no-return-statement】

1、原始版本(版本一):no-return-statement

{

async

function

async1

(){

console

log

‘async1 start’

await

async2

()

console

log

‘async1 end’

}

async

function

async2

(){

console

log

‘async2’

}

setTimeout

(()=>{

console

log

‘setTimeout’

},

0

async1

()

new

Promise

((

resolve

)=>{

console

log

‘promise1’

resolve

()

})。

then

(()=>{

console

log

‘promise2’

})。

then

(()=>{

console

log

‘promise3’

})。

then

(()=>{

console

log

‘promise4’

})

}

此段程式碼在

Chrome v77

中的輸出是:

async1

start

async2

promise1

async1

end

promise2

promise3

promise4

setTimeout

這個輸出其實並沒有違法直覺的地方,是一個很符合邏輯的輸出。

如果讀者對於這一段的輸出還有困惑,建議先了解一下EventLoop以及Promise在EventLoop的執行原理。

之所以強調Chrome版本,是因為

【1.no-return-statement】

在低於v72版本的Chrome又是另一番輸出,感興趣的讀者可以自行測試一下。

為了方便演示,我們把最後一個promise的程式碼修改為:

new

Promise

((

resolve

)=>{

console

log

‘promise’

resolve

()

})。

then

(()=>{

console

log

`%c promise。then1`

`color: blue;`

})。

then

(()=>{

console

log

`%c promise。then2`

`color: blue;`

})。

then

(()=>{

console

log

‘promise。then3’

})。

then

(()=>{

console

log

‘promise。then4’

})。

then

(()=>{

console

log

‘promise。then5’

})

二、轉換對比

此章節強烈建議配合Git程式碼一起看(文末有Git地址)

1、修改版本:return-undefined

按照Async的規範,被async修飾的函式將返回一個promise,即便在函式體內手動return一個原始值,亦被包裝為一個promise。

於是,我們有了

第二個版本

,命名為:

【2.return-undefined】

async

function

async2

(){

console

log

‘async2’

return

undefined

}

此段程式碼在

Chrome v77

中的輸出與

【1.no-return-statement】

async1

start

async2

promise

async1

end

promise

then1

promise

then2

promise

then3

promise

then4

promise

then5

setTimeout

讀者將文末的git專案clone到本地,把對應的HTML檔案放在Chrome中開啟即可。

2、修改版本:async.return-Promise.resolve 與 async.return-new.Promise

如果我們手動return一個promise,會怎樣呢?

得到一個resolve的Promise,可以是:new Promise((resolve, reject)=>{resolve()}),也可以是:Promise。resolve(),我們分別試一下。

a。 返回Promise。resolve:

於是,我們有了

第三個版本

,命名為:

【3.async.return-Promise.resolve】

async

function

async2

(){

console

log

‘async2’

return

Promise

resolve

undefined

}

此段程式碼在

Chrome v77

中的輸出是:

async1

start

async2

promise

promise

then1

promise

then2

async1

end

promise

then3

promise

then4

promise

then5

setTimeout

與版本一

【1.no-return-statement】

或者版本二

【2.return-undefined】

不同的是,“async1 end”輸出滯後兩個時序,被放在了“promise。then2”後面。文末的git專案提供了此版本的程式碼。

很明顯,執行情形已經出現變化了。

b。 返回new Promise:

這樣,我們有了

第四個版本

,命名為:

【4.async.return-new.Promise】

async

function

async2

(){

console

log

‘async2 @ return-Promise。resolve’

return

new

Promise

((

resolve

reject

)=>{

resolve

undefined

)})

}

此段程式碼在

Chrome v77

中的輸出與

【3.async.return-Promise.resolve】

無異,讀者將文末的git專案clone到本地,把對應的HTML檔案放在Chrome中開啟即可。

async1

start

async2

promise

promise

then1

promise

then2

async1

end

promise

then3

promise

then4

promise

then5

setTimeout

其實到這裡已經可以說明一個問題了,那就是在非同步函式中,手動返回一個原始型別的值和手動返回一個promise是有區別的,具體表現在EventLoop的輸出結果上,所以,也就說明了兩者在執行時,

microtask佇列

上是有區別的。

3、修改版本:去除async關鍵詞

我們將async2去除async關鍵詞,修改為一個普通的函式。同時,我們新建一個

中間函式

:asyncMiddle函式,用來返回兩種形式的promise。

結合版本三與版本四,我們可以得到四個版本的程式碼:

版本五:【

5.return-Promise.resolve-return-Promise.resolve

】:

function

asyncMiddle

(){

return

Promise

resolve

async2

()

}

function

async2

(){

console

log

‘async2 @ return-Promise。resolve’

return

Promise

resolve

undefined

}

輸出:

async1

start

async2

promise

async1

end

promise

then1

promise

then2

promise

then3

promise

then4

promise

then5

setTimeout

版本六:【

6.return-Promise.resolve-return-new.Promise

】:

function

asyncMiddle

(){

return

Promise

resolve

async2

()

}

function

async2

(){

console

log

‘async2’

return

new

Promise

((

resolve

reject

)=>{

resolve

undefined

)})

}

輸出:

async1

start

async2

promise

async1

end

promise

then1

promise

then2

promise

then3

promise

then4

promise

then5

setTimeout

版本七:【

7.return-new.Promise-return-Promise.resolve

】:

function

asyncMiddle

(){

return

new

Promise

((

resolve

reject

)=>{

resolve

async2

()

)})

}

function

async2

(){

console

log

‘async2’

return

Promise

resolve

undefined

}

輸出:

async1

start

async2

promise

promise

then1

promise

then2

async1

end

promise

then3

promise

then4

promise

then5

setTimeout

版本八:【

8.return-new.Promise-return-new.Promise

】:

function

asyncMiddle

(){

return

new

Promise

((

resolve

reject

)=>{

resolve

async2

()

)})

}

function

async2

(){

console

log

‘async2’

return

new

Promise

((

resolve

reject

)=>{

resolve

undefined

)})

}

輸出:

async1 start

async2

promise

promise。then1

promise。then2

async1 end

promise。then3

promise。then4

promise。then5

setTimeout

版本七與版本八的表現,與版本三和版本四的輸出表現一致,因此,也可以說:

async

function

fn

(){

return

<

Promise

>

}

在執行上,可以等價於:

function

_fn

(){

return

new

Promise

((

resolve

reject

)=>{

resolve

<

Promise

>

)})

}

到這裡,本文旨意中的第一個已經探究出來了。

需要說明的是,這個結論,僅僅是實驗性的結論。

另有文章也支援等價於這種形式:

function

fn

(){

return

Promise

resolve

<

Promise

>

}

但這個結論無法解釋上述版本之間的輸出區別,因此,筆者在此保留意見,仍以本文得出的結論為主。

三、PromiseResolveThenableJob

我們以相同的輸出,反推了

Async函式

的等價形式,但,為什麼版本七和版本八會延遲兩個時序輸出呢?

或者說:new Promise((rs, rj)=>{rs( )})對比Promise。resolve( ),在微任務的具體執行中有著怎樣差別。

1、Thenable物件

若一個物件或其原型上具有then方法,那麼即可稱該物件為一個Thenable物件

例如:

let thenableObj = {

then(rs, rj){

// [code]

}

}

因此,一個

Promise物件

亦可以稱為一個Thenable物件。

依據TC39對Promise Resolve Functions和PromiseResolveThenableJob的文件說明,簡單來講:

瀏覽器在解析程式碼:new Promise((rs, rj)=>{rs( <thenable> )}) 時,會建立一個“PromiseResolveThenableJob”的微任務

那具體是怎樣的呢?

2、狀態跟隨

對於new Promise((rs, rj)=>{rs( )}),我們也可以稱:newPromise的狀態是跟隨otherPromise的,簡稱“

狀態跟隨

”。

【狀態跟隨1】

程式碼如下:

const

promiseA

=

new

Promise

((

resolve

reject

)=>{

resolve

‘ccc’

})

const

promiseB

=

new

Promise

((

resolve

reject

)=>{

resolve

promiseA

})

promiseB

then

(()=>{

console

log

‘promiseB then’

})

promiseA

then

(()=>{

console

log

‘promiseA then’

})

Promise

resolve

()。

then

(()=>{

console

log

‘p。then1’

})。

then

(()=>{

console

log

‘p。then2’

})。

then

(()=>{

console

log

‘p。then3’

})。

then

(()=>{

console

log

‘p。then4’

})。

then

(()=>{

console

log

‘p。then5’

})。

then

(()=>{

console

log

‘p。then6’

})

輸出:

promiseA

then

p

then1

p

then2

promiseB

then

p

then3

p

then4

p

then5

p

then6

解析:

瀏覽器在解析promiseB的時候,發現其new Promise中resolve的是另一個Thenable物件(另一個例項Promise),會建立一個PromiseResolveThenableJob的微任務來完成轉換工作,等到promiseA被resolved之後,promiseB才會被resolved。

PromiseResolveThenableJob大致會生成如下的虛擬碼:

promiseA。then(()=>{

resolvePromiseB,

rejectPromiseB

})

微任務解析:

步驟1

執行

執行main

stack

生成PromiseResolveThenableJob1微任務

生成console

log

‘promiseA then’

微任務

console

log

‘p。then1’

微任務

微任務佇列

PromiseResolveThenableJob1

console

log

‘promiseA then’

console

log

‘p。then1’

輸出

<

>

步驟2

執行

執行微任務PromiseResolveThenableJob1

生成resolvePromiseB微任務

微任務佇列

console

log

‘promiseA then’

console

log

‘p。then1’

resolvePromiseB

輸出

<

>

步驟3

執行

執行微任務console

log

‘promiseA then’

微任務佇列

console

log

‘p。then1’

resolvePromiseB

輸出

promiseA

then

步驟4

執行

執行微任務console

log

‘p。then1’

生成console

log

‘p。then2’

回撥微任務

微任務佇列

resolvePromiseB

console

log

‘p。then2’

輸出

p

then1

步驟5

執行

執行微任務resolvePromiseB

之後完後promiseB才算是真正地被resolved

),

生成console

log

‘promiseB then’

微任務佇列

console

log

‘p。then2’

console

log

‘promiseB then’

輸出

<

>

步驟6

執行

執行微任務console

log

‘p。then2’

生成console

log

‘p。then3’

回撥微任務

微任務佇列

console

log

‘promiseB then’

console

log

‘p。then3’

輸出

p

then2

步驟7

執行

執行微任務console

log

‘promiseB then’

微任務佇列

console

log

‘p。then3’

輸出

promiseB

then

步驟8

執行

執行微任務console

log

‘p。then3’

生成console

log

‘p。then4’

回撥微任務

微任務佇列

console

log

‘p。then4’

輸出

p

then3

步驟

……

針對這個過程分析可以總結出兩點:

a。

瀏覽器解析類似new Promise((rs, rj)=>{rs( <other-promise> )})的程式碼時,會在微任務中插入一個PromiseResolveThenableJob微任務

。如果被追隨的Promise的狀態被resolved,會立即插入追隨者的resolvePromise微任務。正因為這兩個微任務的存在,才導致了對應的輸出延遲了兩個時序。

b。

對於鏈式的then回撥,瀏覽器只有在執行完畢上一個then回撥後,才會把當前then的回撥新增到微任務佇列末尾

3、Promise.resolve( <other-promise> )

這裡直接說一個結論:按照TC39規範,Promise。resolve( )的執行,會直接返回,不會像額外的生成Job微任務。

上述的各個版本之間的對比,其實也是這個結論的一個佐證,讀者可以細細品味。

四、案例分析

1、重新看待版本七和版本八

如果我們再回頭重新觀察版本七和版本八,其實它們中也各包含一個“狀態追隨”的例項在裡面。

以版本八為例,版本八程式碼:

function

asyncMiddle

(){

return

new

Promise

((

resolve

reject

)=>{

resolve

async2

()

)})

}

function

async2

(){

console

log

‘async2’

return

new

Promise

((

resolve

reject

)=>{

resolve

undefined

)})

}

可以為認為:asyncMiddle追隨async2的狀態。

因此,對應的PromiseResolveThenableJob虛擬碼如下:

async2

then

(()=>{

resolvePromiseAsyncMiddlePromise

rejectPromiseAsyncMiddlePromise

})

微任務分析(我們暫且把setTimeout這個macrotask忽略掉):

步驟1

執行

執行main

stack

生成PromiseResolveThenableJob1微任務

生成console

log

`%c promise。then1`

`color: blue;`

回撥微任務

微任務佇列

PromiseResolveThenableJob1

console

log

`%c promise。then1`

`color: blue;`

輸出

async1

start

async2

promise

如果讀者對這一步的輸出仍然存在疑惑

可以再複習一下EventLoop

步驟2

執行

執行微任務PromiseResolveThenableJob1

生成resolvePromiseAsyncMiddlePromise微任務

微任務佇列

console

log

`%c promise。then1`

`color: blue;`

resolvePromiseAsyncMiddlePromise

輸出

<

>

步驟3

執行

執行微任務console

log

`%c promise。then1`

`color: blue;`

生成console

log

`%c promise。then2`

`color: blue;`

回撥微任務

微任務佇列

resolvePromiseAsyncMiddlePromise

console

log

`%c promise。then2`

`color: blue;`

輸出

promise

then1

步驟4

執行

執行微任務resolvePromiseAsyncMiddlePromise

生成console

log

`%c async1 end`

`color: red;`

回撥微任務

微任務佇列

console

log

`%c promise。then2`

`color: blue;`

console

log

`%c async1 end`

`color: red;`

輸出

<

>

步驟5

執行

執行微任務console

log

`%c promise。then2`

`color: blue;`

生成console

log

‘promise。then3’

回撥微任務

微任務佇列

console

log

`%c async1 end`

`color: red;`

console

log

‘promise。then3’

輸出

promise

then2

步驟5

執行

執行微任務console

log

`%c async1 end`

`color: red;`

微任務佇列

console

log

‘promise。then3’

輸出

async1

end

步驟

……

五、Await轉換

await指令後面可以是一個原始值,也可以是一個Promise物件,同樣,可以將await轉換成promise語法。

僅從瀏覽器平臺角度考慮,這裡需要區分Chrome v73以上版本的實現,和v73以下版本的實現(因為前後的實現確實有區別)。

以Chrome v63版本為基礎,上述

版本一

的程式碼:

async

function

async1

(){

console

log

‘async1 start’

await

async2

()

console

log

‘async1 end’

}

會被轉換成:

function

_async1

(){

console

log

‘async1 start’

let

implicit_promise

=

new

Promise

((

resolve

reject

)=>{

let

promise

=

new

Promise

((

rs

rj

)=>{

rs

async2

()

})

promise

then

(()=>{

console

log

‘async1 end’

resolve

()

})

})

return

implicit_promise

}

其中的:

let

promise

=

new

Promise

((

rs

rj

)=>{

rs

async2

()

})

就是一個“狀態跟隨”。

轉換依據:

探討:當AsyncAwait的遇到了EventLoop

V8對await的抓換處理(舊版實現)

Chrome v63

上的輸出為:

async1

start

async2

promise1

promise2

promise3

async1

end

promise4

setTimeout

對比在

Chrome v77

上的輸出,可以看到,延遲了兩個時序。

而在

Chrome v77

(v73以上版本)上,

版本一

被抓換為:

function

_async1

(){

console

log

‘async1 start’

let

implicit_promise

=

new

Promise

((

resolve

reject

)=>{

let

promise

=

Promise

resolve

async2

()

promise

then

(()=>{

console

log

‘async1 end’

resolve

()

})

})

return

implicit_promise

}

依據上述的論述,由let定義的promise可以被看成是async2返回的promise,因此,就不會有額外的微任務產生。

抓換依據:

探討:當AsyncAwait的遇到了EventLoop

V8對await的抓換處理(新版實現)

額外地,如果是“鏈式”的“狀態跟隨”會怎樣?

例如,

【狀態跟隨2】

const

promiseA

=

new

Promise

((

resolve

reject

)=>{

resolve

‘ccc’

})

const

promiseB

=

new

Promise

((

resolve

reject

)=>{

resolve

promiseA

})

const

promiseC

=

new

Promise

((

resolve

reject

)=>{

resolve

promiseB

})

const

promiseD

=

new

Promise

((

resolve

reject

)=>{

resolve

promiseC

})

promiseD

then

(()=>{

console

log

‘promiseD then’

})

promiseC

then

(()=>{

console

log

‘promiseC then’

})

promiseB

then

(()=>{

console

log

‘promiseB then’

})

promiseA

then

(()=>{

console

log

‘promiseA then’

})

Promise

resolve

()。

then

(()=>{

console

log

‘p。then1’

})。

then

(()=>{

console

log

‘p。then2’

})。

then

(()=>{

console

log

‘p。then3’

})。

then

(()=>{

console

log

‘p。then4’

})。

then

(()=>{

console

log

‘p。then5’

})。

then

(()=>{

console

log

‘p。then6’

})

讀者可以git clone本文的所有程式碼,嘗試在本地執行,如果不太理解輸出,程式碼中也會有微任務步驟分析。

至此,本文旨意中的第二個已經論述得差不多了,希望能對大家有所幫助。

六、總結

本文主要論述了兩點內容:

1、

async函式

轉換成普通函式時,該怎樣的手動返回promise。依據對照試驗結果,應當是返回new Promise式的promise。

2、new Promise在處理一個另一個promise時與Promise。resolve的區別。

3、await轉換成promise的結果以及新舊兩個版本的結果區別,並因此導致的輸出執行差異。

本例的github:thesis-asyncfunc-return

期待點贊;不足之處,歡迎指出。

2019-10-16

前端小知識 @ ctimezliang

標簽: console  log  resolve  promise