每天一小步 <从一道面试题重新理解async/await>

150 阅读3分钟

前言

让我们先来看看下面这道面试题👇

async function async1() {
    console.log("async1 start");
    await new Promise(resolve => {
        console.log("promise1");
        resolve("promise resolve");
    });
    console.log("async1 success");
    return "async1 end";
}

console.log("script start");
async1().then(res => {
    console.log(res);
});

new Promise(resolve => {
    console.log("promise2");
    setTimeout(() => {
        console.log("timer");
    });
});

其实并不是一道难题,依旧是经典的宏任务微任务来回切换,有一个需要注意的点就是async1中的resolve是不会生效的,因为async函数只会把return的内容当作Promise对象返回

下面是答案👇

'script start'
'async1 start'
'promise1'
'promise2'   
'async1 success'
'async1 end'
'timer'  

看到这里或许你会觉得我在水经验-.-,但是其实我真正要说的是下面这一点

为什么promise2会比async1 success先输出呢?

要回答上面的问题,我认为有必要先对async/await的原理有一个简单的了解

async/await是怎么实现的

在这里推荐由林三心大佬写的7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?,相信在看完之后你会对async/await的原理有一个比较深入的理解,下面的内容也是对这篇文章的简单概括

回到主题,async/await其实本质上是由generatoren 生成器实现的,在生成器中可以使用以下的代码实现串行输出👇

function* gen() {
  yield 1
  yield 2
  yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }

由此大胆提出猜测async/awaitnext()函数放到了then()方法中执行,也就是说第一个await函数之前的所有同步操作其实是同步执行的,而第一个await之后的函数其实就被放在了then()中以链式回调的方式执行,这也就是为什么async/await能在单个函数中实现异步函数同步执行的原因

看到这里你可能还是有点懵,没关系,让我们来看看下面的async/await的实现👇

function generatorToAsync(generatorFn) {
  return function() {
    const gen = generatorFn.apply(this, arguments) // gen有可能传参

    // 返回一个Promise
    return new Promise((resolve, reject) => {
      function go(key, arg) {
        let res
        try {
          res = gen[key](arg) // 这里有可能会执行返回reject状态的Promise
        } catch (error) {
          return reject(error) // 报错的话会走catch,直接reject
        }

        // 解构获得value和done
        const { value, done } = res
        if (done) {
          // 如果done为true,说明走完了,进行resolve(value)
          return resolve(value)
        } else {
          // 如果done为false,说明没走完,还得继续走
          // value有可能是:常量,Promise,Promise有可能是成功或者失败
          return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
        }
      }
      go("next") // 第一次执行
    })
  }
}

const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res))

这里面有一句代码能够很好的解释上面的猜测

// 这里将`next()`函数的处理放到了`then()`中执行
return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))

也就是说,next()放到then中执行的思路是没有问题的,那么我们就可以对面试题的问题进行解释了

为什么promise2会比async1 success先输出呢

有了上面的铺垫,要回答这个问题就不难了

async1 success其实是被放到了then中执行,而promise2是同步代码。根据eventloop的执行顺序,在执行完promise1同步任务后就继续执行promise2同步任务,等同步任务队列执行完后,再从微任务队列中取出async1 success执行,这也就是为什么promise2会比async1 success先执行的原因

async function async1() {
    console.log("async1 start");
    await new Promise(resolve => {
        console.log("promise1");
        resolve("promise resolve");
    });
    // 这里的代码其实被放到了then()中执行,也就是放到了微任务队列中,先执行
    console.log("async1 success");
    return "async1 end";
}

console.log("script start");
async1().then(res => {
    // 这里的微任务是在async函数的链式调用的,就会后执行
    console.log(res);
});

new Promise(resolve => {
    console.log("promise2");
    setTimeout(() => {
        console.log("timer");
    });
});

结语

本文是我对这道面试题的一些个人见解,早上看到大佬的文章,下午刷题的时候就碰到了,就想着记录一下自己的理解

如果有任何错误和需要修改的地方,恳请指出,感激不尽~