前言
如果你之前跟我一樣一直對async
await
熟悉又陌生的話(熟悉是可能每天都在用,陌生是針對一些組合題又丈二和尚摸不著頭腦),不妨可以邊看邊練,總結規律,相信會逐漸清晰並有所得。本文對每個案例都詳細描述了程式碼的執行流程,如有不妥歡迎指正。
async 函式return值
async
函式預設會返回一個Promise
物件,不管最後函式有沒有return
值。但是針對具體的返回值情況,實際上表現會有所不同,下面分別看看。
return值為普通值
這裡的普通值是指基礎型別值(Number
、String
和Boolean
等)和非thenable
和非Promise
的值
async function foo() {
return 'foo'
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 輸出結果:a b c
很簡單,不出意外輸出a b c
,也就是async
函式在執行完成後是沒有等待的。
foo()
執行完成沒有等待,遇到then
將console.log('a')
放入微任務佇列;- 繼續往下執行
Promise.resolve()
,遇到then
將console.log('b')
入隊,當前同步任務全部執行完成; - 開始執行微任務佇列,首先取出並執行
console.log('a')
輸出a
; - 然後取出並執行
console.log('b')
輸出b
,此時遇到then
將console.log('c')
入隊; - 最後取出並執行
console.log('c')
輸出c
,至此微任務佇列清空,程式碼執行結束;
return值為thenable
所謂值為thenable
是指定義了then
方法的物件,可以是一個字面物件,也可以是一個Class
例項。
class Bar {
then(resolve) {
resolve()
console.log('then')
}
}
async function foo() {
// return new Bar()
return {
then(resolve, reject) {
resolve()
console.log('then')
}
}
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 輸出結果:then b a c
怎麼順序不一樣了呢?
如果async
函式的返回值是一個thenable
,等同於生成一個Promise
,在foo
函式執行完成,並且Promise
狀態變更(resolve
或者reject
)後,還要等1個then
的時長
foo()
返回thenable
值,執行then
方法,Promise
狀態變更,執行console.log('then')
輸出then
,等待1個then
時長;- 繼續往下執行
Promise.resolve()
,遇到then
將console.log('b')
放入微任務佇列,當前同步任務執行完成; - 開始執行微任務佇列,首先取出並執行
console.log('b')
輸出b
,當前微任務佇列清空; - 此時步驟1等待時長到期,遇到
then
將console.log('a')
放入佇列,取出執行輸出a
; - 繼續步驟3遇到
then
將console.log('c')
放入佇列,取出執行輸出c
,至此微任務佇列清空,程式碼執行結束;
這裡如果foo
函式返回的thenable
方法的狀態沒有變更,則後面的foo().then
將永遠不會執行。
async function foo() {
return {
then(resolve, reject) {
console.log('then')
}
}
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
// 輸出結果:then b c
return 值為Promise
return
後面的值是Promise
,比如 new Promise(resolve=>resolve())
和Promise.resolve
。
async function foo() {
return Promise.resolve('foo')
}
foo().then(() => console.log('a'))
Promise.resolve()
.then(() => console.log('b'))
.then(() => console.log('c'))
.then(() => console.log('d'))
// 輸出結果:b c a d
明顯可以看出async
函式執行完後延遲了2個then
時長。
foo()
返回Promise
值,Promise
狀態變更,等待2個then
時長;- 繼續往下執行
Promise.resolve()
,遇到then
將console.log('b')
放入微任務佇列,當前同步任務執行完成; - 開始執行微任務佇列,首先取出並執行
console.log('b')
輸出b
,當前微任務佇列清空; - 遇到
then
將console.log('c')
放入佇列,取出執行輸出c
; - 此時步驟1等待時長到期,遇到
then
將console.log('a')
放入佇列,取出執行輸出a
; - 繼續步驟4遇到
then
將console.log('d')
放入佇列,取出執行輸出d
,至此微任務佇列清空,程式碼執行結束;
綜合上述表現可以總結出如下規律
await 表示式值
既然async
函式返回值對程式碼執行順序有影響,那麼await
後面的表示式值是否也有影響呢?下面同樣分為上述三種場景進行實驗分析
await值為普通值
async function foo() {
await 'foo'
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
// 輸出結果:a c b d
可以判斷,await
後面的表示式值如果是普通值,無須等待then
時長。那麼,為什麼b
會在c
後面輸出呢?
在await
表示式有執行結果後,await
下一行到函式結束部分程式碼codex
可以看做放置到微任務佇列中,等同於Promise.resolve(await xxx).then(()=>codex)
,這裡是虛擬碼,await
在時間順序上等效於Promise.prototype.then
。
await 'foo'
執行完成後,console.log('a')
被新增到微任務佇列;- 繼續往下執行同步任務
Promise.resolve()
,遇到then
將console.log(c)
新增到微任務佇列,當前同步任務執行完成; - 然後執行微任務佇列中任務,取出並執行
console.log('a')
輸出a
; - 此時
foo
函式執行完成,遇到then
將console.log('b')
入隊; - 繼續執行微任務佇列中
console.log('c')
輸出c
,此時遇到then
將console.log('d')
入隊; - 最後依次執行取出剩餘微任務,執行並輸出
b
和d
,至此微任務佇列清空,程式碼執行結束;
await值為thenable
async function foo() {
await {
then(resolve) {
resolve()
console.log('then')
}
}
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// 輸出結果 then c a d b e
await
後面表示式值如果是thenable
,需要等待1個then
時長,才會去執行後續程式碼。
foo()
執行await
是一個thenable
,Promise
狀態變更,執行同步程式碼console.log('then')
,輸出then
,此時等待1個then
時長;- 繼續往下執行同步任務
Promise.resolve()
,遇到then
將console.log('c')
加入到微任務佇列,當前同步任務執行完成; - 開始執行微任務佇列,取出並執行
console.log('c')
,輸出c
,微任務佇列清空; - 此時步驟1等待時長到期,將
await
後續程式碼console.log('a')
入隊; - 繼續步驟3,遇到
then
將console.log('d')
入隊,然後依次取出console.log('a')
和console.log('d')
並執行,輸出a
和d
; - 執行完
console.log('d')
遇到then
將console.log('e')
放入佇列,取出執行,輸出e
;
確實有點繞,我們將1個then
等待時長看做是下一個微任務從入隊到執行完成出隊的時間就好。比如這裡c
任務執行完成,下一個任務d
正準備進入被a
插了隊。
await值為Promise
async function foo() {
await Promise.resolve('foo')
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// 輸出結果 a c b d e
await
後面表示式如果是Promise
,和普通值的結果是一樣,無須等待then
時長。
為什麼不和return
為Promise
的情景一樣是2次呢?原來這是nodejs
在後期版本最佳化後的結果:移除了2個微任務,1個throwaway promise
,具體原因可以檢視「譯」更快的 async 函式和 promises。
。
對於早期版本(node 11
及以前),輸出的結果是c d a e b
,需要等待2個then
等待時長。
foo()
執行await
是一個Promise
,Promise
狀態變更,此時等待2個then
時長;- 繼續往下執行同步任務
Promise.resolve()
,遇到then
將console.log('c')
加入到微任務佇列,當前同步任務執行完成; - 開始執行微任務佇列,取出並執行
console.log('c')
,輸出c
,微任務佇列清空; - 遇到
then
將console.log('d')
入隊,去除並執行,輸出d
,微任務佇列清空; - 此時步驟1等待時長到期,將
await
後續程式碼console.log('a')
入隊; - 繼續步驟4,遇到
then
將console.log('e')
入隊,然後依次取出console.log('a')
和console.log('e')
並執行,輸出a
和e
; - 執行完
console.log('a')
遇到then
將console.log('b')
放入佇列,取出執行,輸出b
;
綜合await
表示式值的結果,我們可以總結
綜合async await
以上我們僅僅從async
的return
值和await
表示式值單一視角來看,下面綜合他們兩個來分析(統一在node 12+
環境)。
await一個普通函式
首先,await是一個普通函式(非async
函式)
function baz() {
// console.log('baz')
// return 'baz'
// return {
// then(resolve) {
// console.log('baz')
// resolve()
// }
// }
return new Promise((resolve) => {
console.log('baz')
resolve()
})
}
async function foo() {
await baz()
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// await baz函式return是普通值 輸出結果是baz a c b d e
// await baz函式return是thenable 輸出結果是 baz c a d b e
// await baz函式return是Promise 輸出結果 baz a c b d e
與直接await
表示式值輸出一致。
- baz函式
return
是普通值,不等待then
時長; - baz函式
return
是thenable
,等待1個then
時長; - baz函式
return
是Promise
,不等待then
時長;
await一個async函式
然後將baz
函式改成async
async function baz() {
// console.log('baz')
// return 'baz'
// return {
// then(resolve) {
// console.log('baz')
// resolve()
// }
// }
return new Promise((resolve) => {
console.log('baz')
resolve()
})
}
async function foo() {
await baz()
console.log('a')
}
foo().then(() => console.log('b'))
Promise.resolve()
.then(() => console.log('c'))
.then(() => console.log('d'))
.then(() => console.log('e'))
// await baz函式return是普通值 輸出結果是baz a c b d e
// await baz函式return是thenable 輸出結果是 baz c a d b e
// await baz函式return是Promise 輸出結果 baz c d a e b
// node12 以下版本 await baz函式return是Promise 輸出結果 baz c d e a b
從中我們可以發現:await
async
函式的等待時長與async baz
函式的return
值等待時長保持一致。
- async baz函式
return
是普通值,不等待then
時長; - async baz函式
return
是thenable
,等待1個then
時長; - async baz函式
return
是Promise
,等待2個then
時長,但是在node12
以下版本會等待3個then
時長;
綜合async、await、Promise、then和setTimeout
下面我們綜合async
、await
、Promise
、then
和setTimeout
來看一道題目
const async1 = async () => {
console.log('async1')
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise((resolve) => {
console.log('promise1')
resolve()
})
console.log('async1 end')
return Promise.resolve('async1 success')
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
.then(Promise.resolve(2))
.catch(3)
.then((res) => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
思考幾分鐘,輸出結果
// script start
// async1
// promise1
// script end
// async1 end
// 1
// async1 success
// timer2
// timer1
- 執行同步任務輸出
script start
和async1
,遇到setTimeout
放入宏任務佇列; - 繼續往下執行
await
表示式,執行new Promise
輸出promise1
,Promise
狀態變更,不等待then
時長,將後續程式碼新增到微任務佇列; - 繼續往下執行輸出
script end
,執行Promise.resolve(1)
遇到then
將Promise.resolve(2)
放入微任務佇列; - 再往下執行遇到
setTimeout
放入宏任務佇列,至此同步任務執行完畢; - 開始執行微任務佇列,取出並執行步驟2的後續程式碼輸出
async1 end
,返回一個已變更的Promise
物件,需要等待2個then
時長; - 繼續取出微任務
Promise.resolve(2)
並執行,狀態為resolved
後面走then
; - 遇到
then
將(res) => console.log(res)
放入微任務佇列,然後取出並執行輸出1
,注意:then
中是非函式表示式會執行,預設返回的是上一個Promise
的值,then(Promise.resolve(2))
會透傳上一層的1
; - 此時步驟5等待時長到期,將
(res) => console.log(res)
放入微任務佇列,然後取出並執行輸出async1 success
; - 最後2個定時器分別到期,輸出
timer2
和timer1
;
如果對這個案例再稍作改造
const async1 = async () => {
console.log('async1')
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise((resolve) => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start')
async1().then((res) => console.log(res))
console.log('script end')
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then((res) => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)
// 輸出結果:
// script start
// async1
// promise1
// script end
// 1
// timer2
// timer1
具體過程就不一一列舉了,從輸出結果可以發現:如果await
表示式的Promise
的狀態沒有變更,以下程式碼以及後面的then
永遠都不會執行。then
的執行時機是在前面函式執行完成並且Promise
狀態變更以後才會被新增到微任務佇列中等待執行。
總結
透過以上就是基本async
await
的使用場景,以及綜合then
、Promise
和setTimeout
的混合使用,大致可以總結如下幾條規律:
async
函式的return
值為thenable
會等待1個then
時長,值為Promise
會等待2個時長;await
表示式值為thenable
會等待1個then
時長,值為Promise
在node12+
不等待then
時長,低版本node
等待2個then
時長;await
一個async
函式,async
函式的return
值為thenable
會等待1個then
時長,值為Promise
在node12+
會等待2個then
時長,在低版本node
等待3個then
時長;- 如果
then
中是非函式,表示式本身會執行,預設返回的是上一個Promise
的值,也就是透傳上一個Promise
結果; - 如果
await
表示式的Promise
的狀態沒有變更,以下程式碼以及後面的then
永遠都不會執行;
以上案例均透過實驗執行得出,流程如有解釋錯誤,歡迎指正,完~