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