从字节面试题了解 async/await 执行顺序
一道字节面试题:执行顺序
今天看牛客网字节跳动前端面经,发现一道题目:
// 说出下面代码的输出
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
这道题乍看也是一道普通的执行顺序题,对微任务宏任务,event loop 了解一下就可以做出来了。比较有意思的是其中 async/await 这一块,第一次做的时候还真做错了。做错了咋办?学啊,这就打开阮一峰ES6教程开学。
不过在此之前,还是先给出上面这道题的输出,你做对了吗?
async函数的基本性质
在阮一峰ES6教程中,开篇就给出了一个结论:
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
Generator函数的作用是什么呢?根据我个人的理解,就是生成一种 协程,一种可以中断的操作。在生成器函数中,用yield控制生成器的开始和停止。而这种语法可以用 async/await 改写。
// 考虑这样一个生成器
function* generatorFn() {
yield 'foo';
yield 'bar';
retrun 'baz';
}
// 可以改写为
async function asyncFn() {
await 'foo';
await 'bar';
retrun 'baz';
}
// 一比较就会发现,`async`函数就是将 Generator 函数的星号(`*`)替换成`async`,将`yield`替换成`await`,仅此而已。
那么既然一样,为什么要有async函数呢?ES6教程中写了四个原因:
①内置执行器,不用再去用 next()
。
②更好的语义。表明了其中有异步操作。
③更广的适用性。co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
这一块我没懂...
④返回值是 Promise。可以用then()
指定接下来的操作。
但是说了这么多,为什么它的执行顺序是上图那样呢?别急,接来下讲。
async函数的执行顺序
想要了解async函数看起来比较费解的输出顺序,我们需要了解它到底属于什么。在哪个任务队列中。
教程中与之有关的有三个部分:
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
正常情况下,
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
这里明确了三个事情:
1. async
函数返回一个 Promise 对象,所以它是放在微任务中的
2. await
命令后面是一个 Promise 对象或者一个普通的值
3. async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变
ok,其实了解了这三件事情,我们就可以回到刚刚那道面试题了。
面试题再分析
首先我们还是把题目再看一遍:
// 说出下面代码的输出
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
1.开启第一次宏任务
1.1 我们找到了 async1 和 async2 的函数声明,把它们保存到全局对象中。
1.2.我们发现了第一条语句:console.log('script start');
,我们直接输出。
1.3.我们遇到了setTimeout
,把它放在下一次宏任务
1.4.我们遇到了 async1()
,执行它,还记得上面说的吗,它类似于一个 Promise,所以我们可以把它看成类似这样的结构:
new Promise(function (resolve) {
console.log('async1 start');
async2();
resolve();
}).then(
console.log('async1 end');
)
当然实际上肯定不是这样的,但是这样在分析执行顺序的时候比较方便。我来具体解释一下其中的点:
①由上文第三个事实中可知:在await函数都执行完了之后才会进行状态的更改,于是这里只有在async2()
之后才能用resolve()
将pending
状态改为resolved
状态。如果加个await async3()...
,必须等待他们都执行完了才能resolve()
②当 async 函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。所以这里await
之后的语句就会放在then
执行
那么明白了这种处理结构,这部分的顺序也很好理解了。首先直接输出'async1 start'
,并且执行 async2()
,因为它是在 Promise 定义时的语句,输出了'async2'
;。然后将 Promise 状态设置为 resolved, 存入微任务队列中。
1.5.我们遇到了一个 Promise 对象。首先直接输出 promise1
,因为它是在 Promise 定义时的语句。然后将 Promise 状态设置为 resolved, 存入微任务队列中。
1.6.直接执行console.log('script end');
2.至此,第一次宏任务结束,微任务队列不空,所以开始执行微任务队列。
2.1 第一个就是那个 async 函数,它被我们看做一个状态为 resolved 的 Promise。我们执行它的 then
函数,console.log('async1 end');
2.2 第二个就是那个真的状态为 resolved 的 Promise。同样的,我们执行它的 then
函数,console.log('promise2');
3.至此,微任务队列执行完毕,第二次宏任务中,将setTimeout中的函数执行,console.log('setTimeout');
所以上述代码的输出结果是:
1.2 script start
1.4 async1 start; async2
1.5 promise1
1.6 script end
2.1 async1 end
2.2 promise2
3 setTimeout
与之前实际执行结果对比:
可以看出的确是正确的,至此我们解题结束。
参考
转载自:https://juejin.cn/post/7082026952516698148