你不知道的 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