await 中那些不为人知的细节
前言
前置知识:
《其实你不知道 Promise.then 》
《你真的明白 promise 的 then 吗?》
本文为此专栏的第三篇的文章,在之前的文章中我们剖析 v8 引擎 中实现 promise.then 的处处细节,在之前的文章中我们提及了很多 重要的概念 以及 异步模型,为了你拥有更好的阅读体验,极其建议你先看完本专栏的前两篇文章
1. async 函数的返回值
在讨论 await
之前, async
函数是不可避免的话题不可避免
关于 async
函数处理返回值,也像 Promise.prototype.then
一样会对返回值的类型进行辨识:根据返回值的类型,引起 js 引擎
对返回值处理方式的不同
结论:async 函数在抛出返回值会根据返回值类型开启不同数目的 微任务
- return 非
thenable
接口:不落后- return
thenable
接口的非promise
,落后 1 个then
的时间- return
promise
,落后 2 个then
的时间
在将返回值抛出之前仍然会进行 Promise.resolve
包装
就像下面的这三个例子一样
(async function getNumber() {
return 1;
})().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// 1 2 3
(async function getNumber() {
return {
then(cb) {
cb();
},
};
})().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// 2 1 3
(async function getNumber() {
return Promise.resolve();
})().then(() => console.log(1));
Promise.resolve()
.then(() => console.log(2))
.then(() => console.log(3));
// 2 3 1
如果你对这些输出顺序仍然感到困惑,再次建议你回顾本专栏的前两篇文章:
《其实你不知道 Promise.then 》
《你真的明白 promise 的 then 吗?》
async
关键字只是一个函数标识符,在不使用await
情况下,只能通过 控制返回值 从而产生异步的 微任务
2. await 与异步
2.1 await 只是暂停函数执行
await
关键字的作用和 yield
关键字一模一样,都具有控制函数内部的 暂停 与 恢复执行 的能力,如果你不了解 yield
与 生成器
,你可以阅读 《你不知道的生成器 - 掘金 (juejin.cn)》 这篇文章。
async function test() {
console.log(1);
await 1;
console.log(2);
}
test();
console.log(3);
// 1 3 2
执行顺序:
(1)执行 test()
函数,同步执行 函数体中的代码,输出 1
(2)遇到 await
关键字停止执行,因为 await
右边是一个可用的值,所以立即向 微任务队列 添加一个 微任务,这个任务会 恢复异步函数的执行
(3)输出3
,同步线程代码执行完毕
(4)js 运行时从微任务队列取出任务,恢复函数执行,await
去得值 1
,这时候 await 1
的值就是 1
(5)执行函数剩余部分,输出 2
其实在这个过程中,我们更应该关心的是 await
求值的过程,就像下面的代码表现出来的一样
function getNumber() {
console.log(2);
}
async function test() {
console.log(1);
await getNumber();
console.log(3);
}
test();
console.log(4);
// 1 2 4 3
getNumber()
函数先执行,然后再暂停函数,等待同步任务结束后,从微任务队列中取出恢复函数执行的任务并且执行后再将 await
中右边的值赋值给 await
。
2.2 await 求值顺序
如果 await
右边的函数有异步执行的任务,又是什么情况呢
function getNumber() {
console.log(2);
Promise.resolve()
.then(() => console.log(5))
.then(() => console.log(6))
.then(() => console.log(7));
}
async function test() {
console.log(1);
await getNumber();
console.log(3);
}
test();
console.log(4);
// 1 2 4 5 3 6 7
你会发现,await
之后的内容并非会在 getNumber
执行完成之前进行
结论:在时间顺序上
await
之后的内容只会 等待一个 右值内部then
的时间,并非等待 右边的值 全部执行完
2.3 await 右值类型
如果 await 右值是一个 thenable
或者 promise
呢,又是什么情况呢
async function test() {
console.log(1);
await {
then(cb) {
cb();
},
};
console.log(2);
}
test();
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 3 2 4 5 6
如果 await
右边是个 thenable
,await
后面的内容只会等待一个 then
的时间
async function test() {
console.log(1);
await Promise.resolve();
console.log(2);
}
test();
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 2 3 4 5 6
如果 await
右边是个 promise
,await
后面的内容只会为什么表现的和非 thenable
值一样呢?为什么不等待两个 then
的时间呢?
TC 39 对 await 后面是
promise
的情况如何处理进行一次修改,在早期版本,依然会等待两个then
的时间
掘金上有大佬翻译了官方解释:《「译」更快的 async 函数和 promises - 掘金 (juejin.cn)》,但在这次更新中并没有修改 thenable
的情况
这样做的好处,极大优化了 await
等待的速度
async function getNumber() {
console.log(0);
await 1;
console.log(7);
await 2;
console.log(8);
await 3;
console.log(9);
}
async function test() {
console.log(1);
await getNumber();
console.log(2);
}
test();
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 0 7 3 8 4 9 5 2 6
所以你能合并两个函数的代码:
async function test() {
console.log(1);
console.log(0);
await 1;
console.log(7);
await 2;
console.log(8);
await 3;
console.log(9);
await null;
console.log(2);
}
test();
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 0 7 3 8 4 9 5 2 6
又完全可以等效如下代码:
async function test() {
console.log(1);
console.log(0);
Promise.resolve()
.then(() => console.log(7))
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(2));
}
test();
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 0 7 3 8 4 9 5 2 6
这三种写法在时间的顺序上完全等效,所以你 完全可以将 await
后面的代码可以看做在 then
里面执行的结果,又因为 async
函数会返回 promise
实例,所以你还可以等效成:
async function test() {
console.log(1);
console.log(0);
}
test()
.then(() => console.log(7))
.then(() => console.log(8))
.then(() => console.log(9))
.then(() => console.log(2));
Promise.resolve()
.then(() => console.log(3))
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
// 1 0 7 3 8 4 9 5 2 6
这样你会发现,async test
函数内部全是同步的代码,对于你观察 promise
执行顺序是不是更清晰呢。
为什么能做这种等效时间替换呢?
await
会暂停async
函数的执行,等到js 运行时
取出恢复函数执行的微任务并执行,在这个过程中仅仅发生了一个then
的时间
3. 总结
async
函数会根据返回值类型的不同产生0 - 2
个的 微任务,然后对返回值进行Promise.resolve
包装并抛出await
关键字的作用:暂停函数的执行,暂停时间等效为 一个then
的时间,在这之后立即将右值结果赋值给await
,并非等到等到右值完全执行完毕,才会执行后面的内容
结语
到此,如果你完整阅读过 《从 v8 看 promise》专栏中的 三篇文章,相信你足以面对绝大数关于 promise
的面试题,如果再掌握了一点 宏任务
的知识,相信没有你理解不了 promise
执行顺序的问题了。