你不知道的 async、await 魔鬼細節
作者:Squirrel_
https://juejin.cn/post/7194744938276323384
0、前言
關於promise、async/await
的使用相信很多小夥伴都比較熟悉了,但是提到事件循環機制輸出結果類似的題目,你敢說都會?
試一試?
🌰1:
async function async1 () {
await new Promise((resolve, reject) => {
resolve()
})
console.log('A')
}
async1()
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
// 最終結果👉: B A C D
🌰2:
async function async1 () {
await async2()
console.log('A')
}
async function async2 () {
return new Promise((resolve, reject) => {
resolve()
})
}
async1()
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
// 最終結果👉: B C D A
❓基本一樣的代碼爲什麼會出現差別,話不多說👇
1、async 函數返回值
在討論 await
之前,先聊一下 async
函數處理返回值的問題,它會像 Promise.prototype.then
一樣,會對返回值的類型進行辨識。
👉根據返回值的類型,引起 js引擎
對返回值處理方式的不同
📑結論:
async
函數在拋出返回值時,會根據返回值類型開啓不同數目的微任務
return 結果值:非
thenable
、非promise
(不等待)return 結果值:
thenable
(等待 1 個then
的時間)return 結果值:
promise
(等待 2 個then
的時間)
🌰1:
async function testA () {
return 1;
}
testA().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// (不等待)最終結果👉: 1 2 3
🌰2:
async function testB () {
return {
then (cb) {
cb();
}
};
}
testB().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// (等待一個then)最終結果👉: 2 1 3
🌰3:
async function testC () {
return new Promise((resolve, reject) => {
resolve()
})
}
testC().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// (等待兩個then)最終結果👉: 2 3 1
async function testC () {
return new Promise((resolve, reject) => {
resolve()
})
}
testC().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3))
.then(() => console.log(4))
// (等待兩個then)最終結果👉: 2 3 1 4
看了這三個🌰是不是對上面的結論
有了更深的認識?
稍安勿躁,來試試一個經典面試題👇
async function async1 () {
console.log('1')
await async2()
console.log('AAA')
}
async function async2 () {
console.log('3')
return new Promise((resolve, reject) => {
resolve()
console.log('4')
})
}
console.log('5')
setTimeout(() => {
console.log('6')
}, 0);
async1()
new Promise((resolve) => {
console.log('7')
resolve()
}).then(() => {
console.log('8')
}).then(() => {
console.log('9')
}).then(() => {
console.log('10')
})
console.log('11')
// 最終結果👉: 5 1 3 4 7 11 8 9 AAA 10 6
👀做錯了吧?
哈哈沒關係
步驟拆分👇:
先執行同步代碼,輸出
5
執行
setTimeout
,是放入宏任務異步隊列中接着執行
async1
函數,輸出1
執行
async2
函數,輸出3
Promise
構造器中代碼屬於同步代碼,輸出4
async2
函數的返回值是Promise
,等待2
個then
後放行,所以AAA
暫時無法輸出
async1
函數暫時結束,繼續往下走,輸出7
同步代碼,輸出
11
執行第一個
then
,輸出8
執行第二個
then
,輸出9
終於等到了兩個
then
執行完畢,執行async1
函數里面剩下的,輸出AAA
再執行最後一個微任務
then
,輸出10
執行最後的宏任務
setTimeout
,輸出6
❓是不是豁然開朗,歡迎點贊收藏!
2、await 右值類型區別
2.1、非 thenable
🌰1:
async function test () {
console.log(1);
await 1;
console.log(2);
}
test();
console.log(3);
// 最終結果👉: 1 3 2
🌰2:
function func () {
console.log(2);
}
async function test () {
console.log(1);
await func();
console.log(3);
}
test();
console.log(4);
// 最終結果👉: 1 2 4 3
🌰3:
async function test () {
console.log(1);
await 123
console.log(2);
}
test();
console.log(3);
Promise.resolve()
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7));
// 最終結果👉: 1 3 2 4 5 6 7
Note:
await
後面接非thenable
類型,會立即向微任務隊列添加一個微任務then
,但不需等待
2.2、thenable
類型
async function test () {
console.log(1);
await {
then (cb) {
cb();
},
};
console.log(2);
}
test();
console.log(3);
Promise.resolve()
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7));
// 最終結果👉: 1 3 4 2 5 6 7
Note:
await
後面接thenable
類型,需要等待一個then
的時間之後執行
2.3、Promise
類型
async function test () {
console.log(1);
await new Promise((resolve, reject) => {
resolve()
})
console.log(2);
}
test();
console.log(3);
Promise.resolve()
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7));
// 最終結果👉: 1 3 2 4 5 6 7
❓爲什麼表現的和非 thenable
值一樣呢?爲什麼不等待兩個 then
的時間呢?
Note:
TC 39(ECMAScript 標準制定者) 對
await
後面是promise
的情況如何處理進行了一次修改,移除了額外的兩個微任務,在早期版本,依然會等待兩個then
的時間有大佬翻譯了官方解釋:更快的 async 函數和 promises[1],但在這次更新中並沒有修改
thenable
的情況
這樣做可以極大的優化 await
等待的速度👇
async function func () {
console.log(1);
await 1;
console.log(2);
await 2;
console.log(3);
await 3;
console.log(4);
}
async function test () {
console.log(5);
await func();
console.log(6);
}
test();
console.log(7);
Promise.resolve()
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(10))
.then(() => console.log(11));
// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11
Note:
await
和Promise.prototype.then
雖然很多時候可以在時間順序上能等效,但是它們之間有本質的區別。
test
函數中的await
會等待func
函數中所有的await
取得 恢復函數執行 的命令並且整個函數執行完畢後才能獲得取得 恢復函數執行的命令;也就是說,
func
函數的await
此時不能在時間的順序上等效then
,而要等待到test
函數完全執行完畢;比如這裏的數字
6
很晚才輸出,如果單純看成then
的話,在下一個微任務隊列執行時6
就應該作爲同步代碼輸出了纔對。
所以我們可以合併兩個函數的代碼👇
async function test () {
console.log(5);
console.log(1);
await 1;
console.log(2);
await 2;
console.log(3);
await 3;
console.log(4);
await null;
console.log(6);
}
test();
console.log(7);
Promise.resolve()
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(10))
.then(() => console.log(11));
// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11
因爲將原本的函數融合,此時的 await
可以等效爲 Promise.prototype.then
,又完全可以等效如下代碼👇
async function test () {
console.log(5);
console.log(1);
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(6))
}
test();
console.log(7);
Promise.resolve()
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(10))
.then(() => console.log(11));
// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11
以上三種寫法在時間的順序上完全等效,所以你 完全可以將 await
後面的代碼可以看做在 then
裏面執行的結果,又因爲 async
函數會返回 promise
實例,所以還可以等效成👇
async function test () {
console.log(5);
console.log(1);
}
test()
.then(() => console.log(2))
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(6))
console.log(7);
Promise.resolve()
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(10))
.then(() => console.log(11));
// 最終結果👉: 5 1 7 2 8 3 9 4 10 6 11
可以發現,test
函數全是走的同步代碼...
所以👉:**async/await
是用同步的方式,執行異步操作 **
3、🌰
🌰1:
async function async2 () {
new Promise((resolve, reject) => {
resolve()
})
}
async function async3 () {
return new Promise((resolve, reject) => {
resolve()
})
}
async function async1 () {
// 方式一:最終結果:B A C D
// await new Promise((resolve, reject) => {
// resolve()
// })
// 方式二:最終結果:B A C D
// await async2()
// 方式三:最終結果:B C D A
await async3()
console.log('A')
}
async1()
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
大致思路👇:
首先,**
async
函數的整體返回值永遠都是Promise
,無論值本身是什麼 **方式一:
await
的是Promise
,無需等待方式二:
await
的是async
函數,但是該函數的返回值本身是 ** 非thenable
**,無需等待方式三:
await
的是async
函數,且返回值本身是Promise
,需等待兩個then
時間
🌰2:
function func () {
console.log(2);
// 方式一:1 2 4 5 3 6 7
// Promise.resolve()
// .then(() => console.log(5))
// .then(() => console.log(6))
// .then(() => console.log(7))
// 方式二:1 2 4 5 6 7 3
return Promise.resolve()
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7))
}
async function test () {
console.log(1);
await func();
console.log(3);
}
test();
console.log(4);
步驟拆分👇:
方式一:
同步代碼輸出
1、2
,接着將log(5)
處的then1
加入微任務隊列,await
拿到確切的func
函數返回值undefined
,將後續代碼放入微任務隊列(then2
,可以這樣理解)執行同步代碼輸出
4
,到此,所有同步代碼完畢執行第一個放入的微任務
then1
輸出5
,產生log(6)
的微任務then3
執行第二個放入的微任務
then2
輸出3
然後執行微任務
then3
,輸出6
,產生log(7)
的微任務then4
執行
then4
,輸出7
方式二:
同步代碼輸出
1、2
,await
拿到func
函數返回值,但是並未獲得具體的結果(由Promise
本身機制決定),暫停執行當前async
函數內的代碼(跳出、讓行)輸出
4
,到此,所有同步代碼完畢
await
一直等到Promise.resolve().then...
執行完成,再放行輸出3
方式二沒太明白❓
繼續👇
function func () {
console.log(2);
return Promise.resolve()
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7))
}
async function test () {
console.log(1);
await func()
console.log(3);
}
test();
console.log(4);
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
// 最終結果👉: 1 2 4 B 5 C 6 D 7 3
還是沒懂?
繼續👇
async function test () {
console.log(1);
await Promise.resolve()
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7))
console.log(3);
}
test();
console.log(4);
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
// 最終結果👉: 1 4 B 5 C 6 D 7 3
Note:
綜上,
await
一定要等到右側的表達式有確切的值纔會放行,否則將一直等待(阻塞當前async
函數內的後續代碼),不服看看這個👇
function func () { return new Promise((resolve) => { console.log('B') // resolve() 故意一直保持pending }) } async function test () { console.log(1); await func() console.log(3); } test(); console.log(4); // 最終結果👉: 1 B 4 (永遠不會打印3) // ---------------------或者寫爲👇------------------- async function test () { console.log(1); await new Promise((resolve) => { console.log('B') // resolve() 故意一直保持pending }) console.log(3); } test(); console.log(4); // 最終結果👉: 1 B 4 (永遠不會打印3)
🌰3:
async function func () {
console.log(2);
return {
then (cb) {
cb()
}
}
}
async function test () {
console.log(1);
await func();
console.log(3);
}
test();
console.log(4);
new Promise((resolve) => {
console.log('B')
resolve()
}).then(() => {
console.log('C')
}).then(() => {
console.log('D')
})
// 最終結果👉: 1 2 4 B C 3 D
步驟拆分👇:
同步代碼輸出
1、2
await
拿到func
函數的具體返回值thenable
,將當前async
函數內的後續代碼放入微任務then1
(但是需要等待一個then
時間)同步代碼輸出
4、B
,產生log(C)
的微任務then2
由於
then1
滯後一個then
時間,直接執行then2
輸出C
,產生log(D)
的微任務then3
執行原本滯後一個
then
時間的微任務then1
,輸出3
執行最後一個微任務
then3
輸出D
4、總結
async
函數返回值
📑結論:
async
函數在拋出返回值時,會根據返回值類型開啓不同數目的微任務return 結果值:非
thenable
、非promise
(不等待)return 結果值:
thenable
(等待 1 個then
的時間)return 結果值:
promise
(等待 2 個then
的時間)
await
右值類型區別
接非
thenable
類型,會立即向微任務隊列添加一個微任務then
,但不需等待接
thenable
類型,需要等待一個then
的時間之後執行接
Promise
類型 (有確定的返回值),會立即向微任務隊列添加一個微任務then
,但不需等待TC 39 對
await
後面是promise
的情況如何處理進行了一次修改,移除了額外的兩個微任務,在早期版本,依然會等待兩個then
的時間
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cTBOa9B_7tpJyHDoLAn3JA