前言
如果你之前跟我一樣一直對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永遠都不會執行;
以上案例均通過實驗運行得出,流程如有解釋錯誤,歡迎指正,完~