前端知库

async/await 的原理是什么?怎样用 Generator 实现自动执行?

async 函数可以理解为 Generator 加一个自动执行器:遇到 await 就暂停,等 Promise 完成后带着结果恢复执行,失败就在暂停处抛出错误。手写时用 Generator 的 yield 代替 await,执行器反复调用 next,把每次 yield 出来的值包成 Promise,完成后再把结果传回去。

参考答案

简答

const user = await getUser() 读起来像同步代码,实际上函数在这里暂停了,等 getUser() 返回的 Promise 完成,再带着结果继续往下走。MDN 的说法是,await 会暂停函数的执行,直到 Promise 完成或失败,完成的值就成了 await 表达式的结果。

能“暂停再恢复”的函数,JavaScript 里早就有了,就是 Generator。Generator 函数执行到 yield 会停下来,外面调用 next(value) 才继续,而且传进去的 value 会成为 yield 表达式的结果。所以只要写一个执行器:每次 Generator 吐出一个 Promise,就等它完成,再用 next(结果) 把结果送回去,失败就用 throw(错误) 在暂停的地方抛出,Generator 就能像 async 函数一样运行。

早期没有 async/await 时,co 这类库就是这样做的。可以把 async 函数理解为“Generator 加自动执行器”的语法糖,yield 换成了 await,执行器由引擎内置,返回值包装成 Promise。

要注意,这是理解原理的模型,不是规范里的实现方式。async 函数不需要 Generator 也能运行,引擎有自己的实现,而且 async 函数的错误和栈信息比手写执行器更友好。

手写自动执行器

JavaScript
function run(generatorFn, ...args) {
  return new Promise((resolve, reject) => {
    const iterator = generatorFn(...args);

    function step(method, value) {
      let result;
      try {
        result = iterator[method](value);
      } catch (error) {
        reject(error);
        return;
      }
      if (result.done) {
        resolve(result.value);
        return;
      }
      Promise.resolve(result.value).then(
        (data) => step('next', data),
        (error) => step('throw', error),
      );
    }

    step('next', undefined);
  });
}

const delay = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));

const result = run(function* () {
  const a = yield delay(1, 10);
  const b = yield delay(2, 10);
  try {
    yield Promise.reject(new Error('失败'));
  } catch (error) {
    return `${a + b},捕获到${error.message}`;
  }
});

result.then((value) => console.log(value)); // 3,捕获到失败

第一次调用 step('next', undefined),Generator 开始执行,到第一个 yield 停下,吐出 delay(1, 10) 这个 Promise。执行器等它完成,拿到 1,调用 iterator.next(1),于是 a 就是 1。这正是 const a = await delay(1, 10) 的效果。

yield Promise.reject(...) 那一步,Promise 失败了,执行器调用 iterator.throw(error)。错误在 Generator 暂停的那一行抛出,被里面的 try...catch 捕获,和 async 函数里 try...catch 包住 await 的效果一样。

为什么要用 Promise.resolve(result.value) 包一层?因为 yield 后面不一定是 Promise,也可能是普通值。包一层以后,普通值会马上完成,处理方式就统一了,这和 await 普通值 的行为一致。

iterator[method](value) 外面为什么要 try...catch?如果 Generator 里的错误没被捕获,next 或 throw 调用本身会抛出,这时整个任务失败,要 reject 外层 Promise,相当于 async 函数返回的 Promise 失败了。

await 之后的代码什么时候执行

await 后面的代码总是在微任务里继续执行,哪怕等的是一个已经完成的值。所以:

JavaScript
async function f() {
  console.log(1);
  await null;
  console.log(3);
}
f();
console.log(2);

输出顺序是 1、2、3。f() 同步执行到 await 前打印 1,然后暂停,把后续代码安排为微任务;外面继续执行,打印 2;当前同步代码执行完后,微任务执行,打印 3。事件循环的完整顺序见《事件循环中,任务和微任务按什么顺序执行?》。

常见追问

async 函数一定返回 Promise 吗?

一定。return 1 得到的是一个以 1 完成的 Promise,函数里抛出的错误会变成 Promise 失败。所以调用 async 函数时,同步的 try...catch 捕获不到它的错误,必须 await 它或者用 .catch。

多个 await 写在一起会不会变慢?

如果几个请求互不依赖,写成连续的 await 会一个等一个,总时间是相加的。应该先同时发起,再用 await Promise.all([...]) 一起等,见《async/await 如何处理异常和并发?》。