likes
comments
collection
share

从字节面试题了解 async/await 执行顺序

作者站长头像
站长
· 阅读数 54

一道字节面试题:执行顺序

今天看牛客网字节跳动前端面经,发现一道题目:

// 说出下面代码的输出
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/await 执行顺序

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

与之前实际执行结果对比:

从字节面试题了解 async/await 执行顺序

可以看出的确是正确的,至此我们解题结束。

参考

es6.ruanyifeng.com/#docs/async

转载自:https://juejin.cn/post/7082026952516698148
评论
请登录