聊天讨论 我读了一遍 Babel 编译后的 async/await,终于搞懂了它的原理(附 20 行手写实现)

193577746(kyriewen) · 2026年06月08日 · 14 次阅读

本文从一个真实项目 bug 出发,带你读 Babel 编译结果,然后手写一个最简 async/await。

1. 一个真实的 “翻车” 场景

上周维护一个老项目,看到同事写了这样的代码:

async function processItems(items) {
  const results = [];
  for (let i = 0; i < items.length; i++) {
    const res = await fetch(`/api/process/${items[i]}`);
    results.push(res);
  }
  return results;
}

他把 await 放在 for 循环里,本意是串行请求,结果因为接口响应时间不同,数据顺序全乱了。我帮他改成 Promise.all 后,突然意识到:我其实并不清楚 async/await 底层到底怎么工作的

于是我去看了 Babel 把 async 函数编译成了什么样子——发现它只是一个 generator + 自动执行器 的包装。

这篇文章,我就用 20 行代码,带你手写一个最简版的 async/await


2. 前置知识:Generator 函数

如果你已经熟悉 generator,可以跳过本节。

Generator 是可以暂停和恢复的函数:

function* gen() {
  console.log('step 1');
  yield 1;
  console.log('step 2');
  yield 2;
  return 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: true }

每次调用 next(),函数会执行到下一个 yield 并暂停。
这个特性正好可以用来模拟 await 的 “等待异步结果再继续” 的行为。


3. Babel 编译后长什么样?

写一个最简单的 async 函数:

async function getData() {
  const a = await Promise.resolve(1);
  const b = await Promise.resolve(2);
  return a + b;
}

用 Babel(@babel/preset-env)编译后(简化版),变成了类似这样的代码:

function getData() {
  return _asyncToGenerator(function* () {
    const a = yield Promise.resolve(1);
    const b = yield Promise.resolve(2);
    return a + b;
  })();
}

核心是 _asyncToGenerator 这个辅助函数——它接收一个 generator 函数,并返回一个自动执行该 generator 的函数,最终返回一个 Promise。


4. 手写核心:自动执行器

我们先写一个函数 run(generatorFunc),它能自动执行 generator 直到结束。

function run(generatorFunc) {
  const generator = generatorFunc();   // 获取迭代器对象

  return new Promise((resolve, reject) => {
    function step(nextFunc) {
      try {
        const { value, done } = nextFunc();
        if (done) {
          resolve(value);
        } else {
          // 确保 value 是一个 Promise
          Promise.resolve(value).then(
            (res) => step(() => generator.next(res)),
            (err) => step(() => generator.throw(err))
          );
        }
      } catch (err) {
        reject(err);
      }
    }

    step(() => generator.next()); // 启动执行
  });
}

测试一下

function* myGen() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}

run(myGen).then(console.log); // 输出 3

完美运行。

上面的 run 就是 _asyncToGenerator 最核心的逻辑。真正的 Babel 实现还处理了更多边界情况,但原理完全一致。


5. 封装成真正的 asyncToGenerator

如果你想让函数直接返回 Promise,可以这样封装:

function asyncToGenerator(generatorFunc) {
  return function(...args) {
    const gen = generatorFunc.apply(this, args);
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result;
        try {
          result = gen[key](arg);
        } catch (err) {
          return reject(err);
        }
        const { value, done } = result;
        if (done) {
          resolve(value);
        } else {
          Promise.resolve(value).then(
            v => step('next', v),
            e => step('throw', e)
          );
        }
      }
      step('next');
    });
  };
}

用法:

const getData = asyncToGenerator(function* () {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
});

getData().then(console.log); // 3

和原生 async/await 行为完全一致。


6. 常见误解与踩坑

6.1 await 后面跟着的不是 Promise 会怎样?

await 123 会被隐式转换为 await Promise.resolve(123),所以自动执行器里用 Promise.resolve(value) 包裹是正确的。

6.2 异步错误怎么捕获?

如果 generator 内部 yield 了一个 rejected Promise,自动执行器会调用 generator.throw(err),然后在 try-catch 中 reject 最终的 Promise。所以外层的 .catch 可以捕获。

6.3 for 循环里的 await 是串行还是并行?

// 串行(一个接一个)
for (const id of ids) {
  await fetch(`/api/${id}`);
}

// 并行(同时发起)
await Promise.all(ids.map(id => fetch(`/api/${id}`)));

理解原理后,你就知道为什么串行会慢,以及什么时候该用 Promise.all


7. 总结

  • async/await 的底层 = generator + 自动执行器
  • 手写一个自动执行器只需 20 行左右
  • 真正理解原理后,你就能轻松避免 “异步陷阱”
  • 文中代码可以直接复制到你的项目中跑一跑

讨论:你在项目中遇到过哪些因不理解 async/await 原理而产生的 bug?欢迎在评论区分享你的 “翻车” 经历~

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号