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 只是暂停函数执行
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
的时间
这样做的好处,极大优化了 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
注意:
await
和Promise.prototype.then
虽然很多时候可以在时间顺序上能等效,但是它们之间有本质的区别,就像上面的代码所想表达的一样
test
函数中的 await
会等待 async getNumber
函数中所有的 await
取得 恢复函数执行 的命令并且整个函数执行完毕后才能获得取得 恢复函数执行 的命令,也就是说,async getNumber
函数的 await
此时不能在时间的顺序上等效 then
,而要等待到 test
函数完全执行完毕。
所以 getNumber
函数必定先执行完,test
函数中的 await
才能取得 恢复函数执行的命令,所以你能合并两个函数的代码:
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
此时的 await
可以等效为 Promise.prototype.then
,又完全可以等效如下代码:
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
执行顺序的问题了。
如果本篇文章对你有帮助或者你有不同的看法,欢迎在评论区留下你的足迹。
往期好文推荐:JS中的巨坑 - 数组
转载自:https://juejin.cn/post/7152555291450540069