探討:當AsyncAwait的遇到了EventLoop
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(
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(
狀態跟隨
”。
【狀態跟隨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(
上述的各個版本之間的對比,其實也是這個結論的一個佐證,讀者可以細細品味。
四、案例分析
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
()
)
})
就是一個“狀態跟隨”。
轉換依據:
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,因此,就不會有額外的微任務產生。
抓換依據:
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