async await 執行順序個人理解
本文談一下我對async/await在相對於promise,setTimeout和同步事件在事件迴圈中的執行順序的個人理解,網上鋪墊蓋地的async await執行順序的部落格有很多,但有些說的並不是特別詳細,甚至連巨集任務,微任務都沒有提及,本人在看過那些部落格後針對一些先前不理解的點進行分析
寫在前面
1 本文針對async/await在事件迴圈中的執行順序,但不會詳細的講事件迴圈,讀者需要理解什麼是巨集任務,微任務,事件迴圈,關於事件迴圈可參考這篇部落格 ofollow,noindex">微任務、巨集任務與Event-Loop
2 本人入行不久,寫這篇部落格主要目的是探討交流,有一些個人的理解,如有差異歡迎指出,希望對有同樣問題的人有所幫助
async await是什麼
async函式
MDN上是這麼解釋的
**async function**
宣告用於定義一個返回 JavaScript/">JavaScript/Reference/Global_Objects/AsyncFunction" target="_blank" rel="nofollow noreferrer">AsyncFunction
物件的非同步函式非同步函式是指通過事件迴圈非同步執行的函式,它會通過一個隱式的 Promise
返回其結果
如果沒有明白我們可以列印一個async函式,看看它到底是什麼
async function asyncFn() { console.log('1 am async function') } console.log(asyncFn())

可以看到asyncFn和正常的函式一樣執行函式,輸出了一句l am async function,一般的函式不同的時它返回了一個Promise物件,並且Promise物件的狀態值是resolved,而resolve的值是undefined
再來看一個有return值的async函式
async function asyncFn() { console.log('1 am async function') return 'l have been returned' } console.log(asyncFn())

這時asyncFN函式和之前一樣返回了一個Promise物件,並且狀態是resolved,和之前不同的是它resolve的值是函式return的值
換句話說,async函式會把返回值包裝成一個狀態為resolved的Promise物件,且Promise物件的值為該函式原本返回的值(這裡不考慮rejected的情況)
await關鍵字
await關鍵字只能在async函式中才能使用,用於等待一個await後面的表示式的執行結果
await是一個讓出執行緒的標誌。await後面的函式會先執行一遍,然後就會跳出整個async函式來執行後面js棧的程式碼。等本輪的巨集任務(執行順序優於本輪微任務)執行完了之後又會跳回到async函式中等待await
await後面大多跟著Promise物件,但是同樣可以是一個普通值
- await後面跟著Promise物件時,它會等待Promise物件的狀態變為resolve或者reject,之後會將這個Promise物件resolve或者reject中攜帶的值放入微任務佇列中
2. await後面跟著普通值的時候會直接返回這個值
這裡還有一點需要補充也是我之前一直搞不懂async/await執行順序的原因之一,關於Promise物件,它有3種狀態,pending(等待),resolved(完成),rejected(拒絕),而resolved和rejected狀態如果你不呼叫對應的then()/catch()方法,它是不會返回值的,依然還是一個Promise物件,直到你呼叫then()/catch(),而這2個方法會將Promise的值放入微任務佇列中,才能在微任務佇列中拿到這個返回的值
而await關鍵字和then()類似,await表示式的值等於狀態已經是resolved的Promise物件返回的值,await和then方法一樣,有個關鍵的操作就是把resolved放到微任務佇列中,來看例子
function testPromise() { return new Promise(resolve => { resolve('promise') }) } async function asyncFn() { let res = await testPromise() console.log(res) } asyncFn() console.log('script end')

這裡我聲明瞭一個testPromise函式,執行這個函式會返回一個Promise物件,在asyncFn函式中,所以在asyncFn函式中,await後面跟著的是一個Promise物件,我來分析一下這2個console的列印順序
- 首先asyncFn(),執行asyncFn函式,遇到await關鍵字會執行後面的testPromise函式
- testPromise執行結束後先不會return,而是跳出async函式(讓出執行緒)執行之後的語句
- 接下來列印script end,至此第一輪事件迴圈的巨集任務結束
- 根據await的特性,再次跳轉到async裡面讓await後面的表示式返回結果,而await後面表示式也就是testPromise函式返回了一個狀態為resolved,值為"promise"的Promise物件( 注意此時並沒有從Promise物件中獲得"promise"字串 )
- 因為testPromise返回了一個Promise物件,await會嘗試將這個Promise的值拿出來,也就是將"promise"字串作為回撥放到第一輪事件迴圈的微任務佇列裡,從微任務佇列中可以獲取到"promise"字串
- 在第5步的時候,第一輪事件迴圈巨集任務已經結束,但是微任務還在進行,把"promise"字元放到微任務佇列中後,js會檢視微任務佇列中是否還有任務,如果有則繼續停留在第一輪微任務的執行過程,然後處理微任務佇列中的微任務,直到所有在這一輪微任務佇列中的任務全部完成,第一輪事件迴圈才算真正結束,這時候才開始第二輪事件迴圈的巨集任務佇列("如果在處理微任務的過程中不停的新增微任務同樣會導致阻塞,會一直停留在當輪事件迴圈,並不會開始下一輪")
這裡再用一個相對完整的例子來解釋一下5,6步到底是什麼意思
例子
async function asyncFn() { console.log('asyncFn') let res = await asyncFn2() console.log(res) } async function asyncFn2() { console.log('asyncFn2') let res = await fn3() console.log(res) return 'asyncFn2 return' } function fn3() { console.log('fn3') } setTimeout(() => { console.log('setTimeout') }, 0) console.log('script start') asyncFn() let promise = new Promise(resolve => { console.log('promise') resolve('promise resolved') console.log('after promise resolved') }).then(res => { console.log(res) }) console.log('script end')
控制檯執行結果如下:

我們來逐一分析一下
- 最開始script標籤作為第一輪巨集任務開始
- 遇到setTimeout,setTimeout是巨集任務,將它放在巨集任務佇列中
- 列印"script start"
- 執行asyncFn(),列印"asyncFn"
- 遇到await關鍵字,先執行await關鍵字後面的asyncFn2函式,列印"asyncFn2",再次遇到await關鍵字執行fn3函式列印"fn3"
- 列印完"fn3"後會跳出最外層的async函式(也就是跳出asyncFn函式)
- 新建Promise物件,列印"promise",遇到resolve方法,將"promise resolved"字串放入當前事件迴圈的微任務佇列中,並且將Promise物件的狀態修改為resolved(再次說明一下,Promise物件狀態為resolved/rejected還需要用then()/catch()將Promise物件的值從微任務佇列中取出來才算是獲得該Promise物件的值),隨後列印"after promise resolved"
- 列印"script end",至此第一輪事件迴圈的巨集任務結束
此時微任務佇列有一個任務
巨集任務佇列有一個任務
9.在巨集任務結束後又會跳會async函式中(指的是asyncFn2函式),因為fn3並不是一個async函式,不會返回一個Promise物件,所以await一個沒有return值的普通函式會返回undefined,隨後列印res,即undefined
- asyncFn2函式返回"asyncFn2 return"字串,而這個字串因為async函式,會被包裝成一個狀態為resolved的Promise物件
- 因為asyncFn2函式返回了Promise物件,接著跳轉到asyncFn函式,asyncFn函式中的那個await關鍵字會嘗試將asyncFn2函式的返回值拿出來,也就是說 通過將第10步的那個狀態為resolved值為"asyncFn2 return"的Promise物件放到微任務佇列中,從微任務佇列取出這個Promise物件的值
此時微任務佇列有兩個任務
巨集任務佇列有一個任務
- 而11的操作又會跳出async函式,此時程式開始處理微任務佇列,發現微任務佇列中有第7步Promise物件的resolved的值,隨即執行第7步then方法後面的回撥,列印"promise resolved"
13 .隨後發現微任務佇列中還有一個微任務,即11步放入的微任務,隨後執行回撥,列印"asyncFn2 return",至此,微任務佇列中沒有任務了,第一輪事件迴圈的巨集任務和微任務結束
此時巨集任務佇列有一個任務
14.開始第二輪事件迴圈,從巨集任務開始,發現有之前setTimeout巨集任務放入的回撥,執行回撥列印"setTimeout"
分析部分有點長,建議儲存圖片對比檢視,主要考察了開發者的事件迴圈,promise,非同步這些知識點,如果其中有錯誤,請務必指出,在第一時間修改
感謝觀看