async/await 到底是不是 Generator 的语法糖?手写执行器,Babel 编译产物里藏着答案

Generator 系列第三篇:自动执行器、async/await 与 Generator 的关系、高阶玩法、面试题

系列前两篇我们讲了:

  • 第一篇:Generator 概念、yield/next/throw/return、Iterator 协议
  • 第二篇:Babel 编译产物剖析、switch 状态机、V8 引擎层面的暂停机制

第一篇结尾留了一个问题:async/await 和 Generator 到底什么关系? 当时说了一句"async/await 在引擎层面有自己独立的实现,并不是 Generator 的语法糖"------但没展开。

这篇就来展开。我们从三个角度切入:

  1. 自动执行器 ------手动调 next() 太痛苦,怎么自动执行?
  2. Babel 编译产物------async/await 被 Babel 编译后变成了什么?
  3. 高阶玩法 + 面试题------Generator 在实战中怎么用?

1. 为什么需要自动执行器

1.1 手动 next 的缺点

先回顾一下 Generator 的异步用法:

js 复制代码
function* fetchData() {
  const user = yield getUser(1);       // 请求用户
  const posts = yield getPosts(user.id); // 请求文章
  const comments = yield getComments(posts[0].id); // 请求评论
  return { user, posts, comments };
}

要用这段代码,你得手动调 next()

js 复制代码
const gen = fetchData();

gen.next().value.then((user) => {
  gen.next(user).value.then((posts) => {
    gen.next(posts).value.then((comments) => {
      const result = gen.next(comments).value;
      console.log(result); // { user, posts, comments }
    });
  });
});

三层嵌套就为了拿三个异步结果。这其实和回调地狱没什么区别了。

Generator 让异步代码能"同步写",但"手动执行"的体验太差了。如果有个东西能自动调 next(),拿到 Promise 就等它 resolve,然后自动继续,是不是也可以?

1.2 手写自动执行器

这就是自动执行器(auto-runner)的概念。核心逻辑只有 20 行:

js 复制代码
function autoRun(genFn) {
  return function (...args) {
    const gen = genFn.apply(null, args);

    function step(nextFn) {
      let result;
      try {
        result = nextFn();
      } catch (e) {
        return Promise.reject(e);
      }
      // done = true,Generator 结束
      if (result.done) return Promise.resolve(result.value);

      // 优化点:检查是否是 thenable(Promise 或类 Promise)
      const value = result.value;
      if (value != null && typeof value.then === 'function') {
        // 是 Promise:等它 resolve 后继续 next
        return value.then(
          (val) => step(() => gen.next(val)),
          (err) => step(() => gen.throw(err))
        );
      }
      // 普通值直接同步递归,少一次微任务开销
      return step(() => gen.next(value));
    }

    return step(() => gen.next());
  };
}

逻辑拆解:

  1. gen.next() 拿到 { value, done }
  2. 如果 done 为 true,Generator 结束,value 就是最终结果
  3. 如果 done 为 false,value 通常是一个 Promise,等它 resolve
  4. resolve 后拿值调 gen.next(val)------回到步骤 1
  5. reject 后调 gen.throw(err)------让 Generator 内部的 try/catch 处理

1.3 测试一下

用自动执行器跑一个有条件分支的异步流程:

js 复制代码
// 模拟异步请求
function fetchDetail(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id,
        name: `item-${id}`,
        type: id === 3 ? 'special' : 'normal',
      });
    }, 100);
  });
}

function fetchExtra(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, extra: `extra-data-${id}` }), 100);
  });
}

// Generator 版本
function* processItems(items) {
  const results = [];
  for (const item of items) {
    const detail = yield fetchDetail(item.id);
    if (detail.type === 'special') {
      const extra = yield fetchExtra(detail.id);
      results.push({ ...detail, extra });
    } else {
      results.push(detail);
    }
  }
  return results;
}

// 自动执行
const run = autoRun(processItems);
run([{ id: 1 }, { id: 2 }, { id: 3 }]).then((results) => {
  console.log(results);
});

真实运行输出:

json 复制代码
[
  { "id": 1, "name": "item-1", "type": "normal" },
  { "id": 2, "name": "item-2", "type": "normal" },
  {
    "id": 3, "name": "item-3", "type": "special",
    "extra": { "id": 3, "extra": "extra-data-3" }
  }
]

同样的逻辑用 async/await 写:

js 复制代码
async function processItemsAsync(items) {
  const results = [];
  for (const item of items) {
    const detail = await fetchDetail(item.id);
    if (detail.type === 'special') {
      const extra = await fetchExtra(detail.id);
      results.push({ ...detail, extra });
    } else {
      results.push(detail);
    }
  }
  return results;
}

两个版本输出完全一致。

这就是 Generator + 自动执行器做到的事情------在 async/await 出现之前,前端工程师就是用 Generator + co 库写异步的 。co 是 TJ Hollowaychuk 写的库,本质就是我们上面手写的 autoRun 的完善版。

但这里有个关键问题:async/await 到底是不是 Generator + 自动执行器的语法糖?

这就需要看 Babel 编译后的结果了。


2. Babel 编译结果:async/await 变成了什么

2.1 实验设计

写一个最简单的 async 函数,用 Babel 编译,看结果:

js 复制代码
// 源码
async function fetchUser(id) {
  const resp = await fetch(`/api/users/${id}`);
  const data = await resp.json();
  return data;
}

用 Babel 编译(target: ES5):

bash 复制代码
npx babel source.js --presets='@babel/preset-env' -o compiled.js

2.2 编译结果

js 复制代码
function asyncGeneratorStep(n, t, e, r, o, a, c) {
  try {
    var i = n[a](c), u = i.value;
  } catch (n) {
    return void e(n);
  }
  i.done ? t(u) : Promise.resolve(u).then(r, o);
}

function _asyncToGenerator(n) {
  return function () {
    var t = this, e = arguments;
    return new Promise(function (r, o) {
      var a = n.apply(t, e);
      function _next(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
      }
      function _throw(n) {
        asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
      }
      _next(void 0);
    });
  };
}

function fetchUser(_x) {
  return _fetchUser.apply(this, arguments);
}

function _fetchUser() {
  _fetchUser = _asyncToGenerator(function* (id) {
    const resp = yield fetch(`/api/users/${id}`);
    const data = yield resp.json();
    return data;
  });
  return _fetchUser.apply(this, arguments);
}

其实这么长的代码,核心就是三步:

2.3 产物拆解

第一步:async functionfunction*

js 复制代码
// 源码
async function fetchUser(id) {
  const resp = await fetch(`/api/users/${id}`);
  ...
}

// 编译后
_fetchUser = _asyncToGenerator(function* (id) {
  const resp = yield fetch(`/api/users/${id}`);
  ...
});

async 变成了 function*await 变成了 yield在 Babel 编译层面,async/await 确实被转成了 Generator。

第二步:_asyncToGenerator 就是自动执行器

js 复制代码
function _asyncToGenerator(n) {
  return function () {
    var t = this, e = arguments;
    return new Promise(function (r, o) {
      var a = n.apply(t, e);           // 创建 Generator 实例
      function _next(n) {              // 自动 next
        asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
      }
      function _throw(n) {             // 自动 throw
        asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
      }
      _next(void 0);                   // 启动!
    });
  };
}

和手写的 autoRun 对比:

autoRun(我们写的) _asyncToGenerator(Babel 产物) 本质
gen = genFn() a = n.apply(t, e) 创建 Generator 实例
step(() => gen.next(val)) _next(n)asyncGeneratorStep(..., "next", n) 自动 next
step(() => gen.throw(err)) _throw(n)asyncGeneratorStep(..., "throw", n) 自动 throw
Promise.resolve(result.value).then(...) Promise.resolve(u).then(r, o) 等 Promise resolve 后继续
result.done 判断 i.done ? t(u) : ... 判断是否结束

一模一样。 _asyncToGenerator 就是我们手写的 autoRun 的压缩版。

为什么 asyncGeneratorStep 要用 try/catch 包裹 gen.throw(err)

这不仅仅是为了防御外部 gen.throw 抛错,更关键的是为了支持**await 后面的同步代码报错**。举个例子:

js 复制代码
async function test() {
  await Promise.resolve();
  throw new Error('Oops');  // 这个同步错误怎么被捕获?
}

Babel 会把 throw new Error('Oops') 编译为 return _context.abrupt("throw", new Error('Oops')),最终调用 gen.throw(err)。如果 asyncGeneratorStep 不捕获这个错误,它就会变成未处理的 Promise reject------而真实 async 函数返回的 Promise 本就应该自动捕获内部所有错误。try/catch 兜住 gen.throw(err) 再调用 reject(err),完美复现了原生 async/await 的完整错误传播语义。

第三步:asyncGeneratorStep 是单步执行

js 复制代码
function asyncGeneratorStep(n, t, e, r, o, a, c) {
  try {
    var i = n[a](c), u = i.value;     // n[a](c) = gen.next(val) 或 gen.throw(err)
  } catch (n) {
    return void e(n);                  // 出错 → reject
  }
  i.done ? t(u) : Promise.resolve(u).then(r, o);
  // done → resolve(result)
  // !done → 等 Promise resolve 后继续 _next
}

变量名压缩了(n=gen, t=resolve, e=reject, r=_next, o=_throw, a="next"/"throw", c=value),但逻辑就是:

  1. next()throw()
  2. 拿到 { value, done }
  3. done → 结束
  4. !done → 等 Promise → 继续递归

3. async/await 和 Generator 到底什么关系

看完 Babel 编译结果,async/await 就是 Generator + 自动执行器。

但是Babel 编译 ≠ 引擎实现

3.1 两种视角

视角 结论
Babel 编译视角 async/await → function* + yield + _asyncToGenerator(自动执行器)。确实是"语法糖"
原生引擎视角 async/await 有独立的语义和字节码实现,基于 Promise 微任务调度。不是 Generator 的语法糖

两种视角都对,但说的是不同层面的事。

3.2 原生引擎层面

在原生支持 async/await 的 V8 引擎里:

  • async function 不会被转成 Generator
  • await 不会被转成 yield
  • 引擎底层共享了"上下文挂起/恢复"的机制(类似 Suspend/Resume 的上下文保存与恢复),但 async/await 和 Generator 各自有独立的语义实现和字节码体系
  • 两者在底层能力上有重叠,但表层语义不同

async/await 和 Generator 在引擎层面像是一对表兄弟------共享了"函数暂停/恢复"的底层能力,但各自有独立的人格。

3.3 精确表述

"async/await 是不是 Generator 的语法糖",可以这样回答:

在 Babel 编译层面 ,async/await 确实被转译为 Generator + 自动执行器(_asyncToGenerator)。

在原生引擎层面,async/await 有独立的语义和实现,基于 Promise 微任务调度。它和 Generator 共享了"上下文挂起/恢复"的底层机制,但不是语法糖。

在历史演进层面,async/await 的设计借鉴了 Generator 的"同步风格写异步"思路。Generator 是异步编程演进链上的关键一环------从回调到 Promise,再到 Generator(配合 co 库),最终到 async/await。

一句话总结:Babel 编译层面是语法糖,引擎层面不是。历史传承上,Generator 是 async/await 的"精神前身"。


4. 高阶玩法

4.1 惰性序列

Generator 天然适合做惰性求值------值不需要的时候不计算:

js 复制代码
function* naturals() {
  let n = 1;
  while (true) {       // 无限循环!但不会卡死
    yield n++;
  }
}

function* take(gen, count) {
  for (const x of gen) {
    if (count-- <= 0) return;
    yield x;
  }
}

function* map(gen, fn) {
  for (const x of gen) {
    yield fn(x);
  }
}

// 取前 5 个自然数的平方
const result = [...take(map(naturals(), x => x * x), 5)];
console.log(result); // [1, 4, 9, 16, 25]

naturals() 是无限序列,但因为有 take 限制,只会计算 5 个。这就是惰性求值的威力------无限数据流,按需取用。

4.2 并发控制

Generator + Promise 可以做并发控制------限制同时执行的异步任务数量:

js 复制代码
function* concurrent(tasks, limit) {
  const results = [];
  const executing = new Set();
  let index = 0;

  for (const task of tasks) {
    const currentIndex = index++; // 锁定索引,保证结果顺序

    // 如果达到并发上限,等待最快完成的一个释放槽位
    if (executing.size >= limit) {
      // 注意:这里只用来"等一个坑位",不关心返回值
      yield Promise.race(
        [...executing].map((p) => p.catch(() => {}))
      );
    }

    // 关键修复:用 Promise.resolve() 包裹,将同步抛出转为 reject 的 Promise
    const p = Promise.resolve()
      .then(() => task())
      .then((res) => {
        executing.delete(p);
        results[currentIndex] = res; // 按索引存入,保证顺序
        return res;
      })
      .catch((err) => {
        executing.delete(p);
        results[currentIndex] = err; // 错误也按索引存,方便定位
        throw err; // 保留错误,让 autoRun 的 gen.throw 捕获
      });

    executing.add(p);
  }

  // 等待所有剩余任务完成
  if (executing.size > 0) {
    yield Promise.allSettled(executing);
  }

  return results; // 结果按原始任务顺序排列
}

// 用 autoRun 执行
autoRun(concurrent)(tasks, 2);

原版有两个致命缺陷:Promise.race(executing) 直接用 Set 里的 Promise,任意一个 reject 就会崩溃;② task() 同步抛错时,executing.add(p) 根本执行不到,连锁崩溃。上面的修复:race 的每个 Promise 先 .catch(() => {}) 静默化拒绝,Promise.resolve().then(() => task()) 将同步错误转为 rejected promise,彻底杜绝崩溃;currentIndex 锁住位置,保证结果有序。

真实运行输出(4 个任务,并发限制 2):

less 复制代码
并发结果: ['B', 'A', 'C', 'D']

这个模式的用处:爬虫控制并发、API 限流、批量文件处理。


5. 面试题

Q1:下面代码的输出顺序是什么?

js 复制代码
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((resolve) => {
  console.log('promise');
  resolve();
}).then(() => {
  console.log('then');
});

console.log('script end');

答:

arduino 复制代码
script start → async1 start → async2 → promise → script end
→ async1 end → then → setTimeout
  1. script start → 立即输出
  2. async1() 调用 → 进入 async1async1 start 立即输出
  3. 调用 async2()函数体同步执行async2 立即输出(async2() 本身是同步调用)
  4. await 触发:后面的代码(async1 end)被放入微任务队列
  5. 继续执行同步代码:promise 立即输出,resolve() 调用后 .then 回调也进入微任务队列
  6. script end 输出

然后清空微任务队列:async1 endthen(两者都是微任务,谁先入队谁先出)。最后宏任务 setTimeout

Q2:Generator 的 return 值和 done 的关系

js 复制代码
function* gen() {
  yield 1;
  yield 2;
  return 3;
  yield 4;
}

const g = gen();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());

答:

js 复制代码
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }   // return 让 done 变 true
{ value: undefined, done: true }  // 之后永远 undefined

解析: return 会让 Generator 立即结束,done 变 true。后面的 yield 4 不会执行。

Q3:yield* 委托的返回值

js 复制代码
function* inner() {
  yield 'a';
  yield 'b';
  return 'inner-done';
}

function* outer() {
  yield 1;
  const result = yield* inner();
  console.log('inner 返回:', result);
  yield 2;
}

const o = outer();
console.log(o.next());
console.log(o.next());
console.log(o.next());
console.log(o.next());
console.log(o.next());

答:

js 复制代码
{ value: 1, done: false }
{ value: 'a', done: false }
{ value: 'b', done: false }
inner 返回: inner-done     // yield* 拿到 inner 的 return 值
{ value: 2, done: false }
{ value: undefined, done: true }

解析: yield* 会委托执行 inner Generator,inner 的 return 值会作为 yield* 表达式的返回值。

Q4:for...of 遍历 Generator 会拿到 return 的值吗?

js 复制代码
function* withReturn() {
  yield 1;
  yield 2;
  return 3;
}

const arr = [];
for (const x of withReturn()) {
  arr.push(x);
}
console.log(arr);

答: [1, 2]for...of 只遍历 yield 的值,不包含 return 的值

解析: for...of 看到 done: true 就停了,return 的 3 不在遍历结果里。这是 for...of 和手动 next() 的关键区别。

Q5:async/await 和 Generator 的关系,精确表述

答: 三个层面:

  1. Babel 编译层面 :async/await 被编译为 Generator + _asyncToGenerator(自动执行器),确实是"语法糖"
  2. 原生引擎层面:async/await 有独立的语义和字节码实现,基于 Promise 微任务调度,不是 Generator 的语法糖
  3. 历史演进层面:Generator 是 async/await 的"精神前身"------"同步风格写异步"的思路从 Generator 延续到了 async/await

Q6:手写一个 Generator 自动执行器(核心 20 行)

答:

js 复制代码
function autoRun(genFn) {
  return function (...args) {
    const gen = genFn.apply(null, args);
    function step(nextFn) {
      let result;
      try { result = nextFn(); } catch (e) { return Promise.reject(e); }
      if (result.done) return Promise.resolve(result.value);

      // 优化点:检查是否是 thenable,对普通值直接递归减少微任务开销
      const value = result.value;
      if (value != null && typeof value.then === 'function') {
        return value.then(
          (val) => step(() => gen.next(val)),
          (err) => step(() => gen.throw(err))
        );
      }
      return step(() => gen.next(value));
    }
    return step(() => gen.next());
  };
}

解析: 核心逻辑:调 next() → 拿到 { value, done } → done 就结束 → !done 就等 Promise resolve → 继续调 next(val)。此处采用优化版,对普通值直接递归,减少不必要的微任务开销;核心逻辑与基础版完全一致。Babel 的 _asyncToGenerator 也是这个逻辑,只是变量名被压缩了。


6. 系列总结

三篇文章,从"会用"到"懂原理":

主题 核心结论
第一篇 概念与语法 Generator 是可以暂停/恢复的函数,yield 暂停,next 恢复
第二篇 暂停原理 Babel 方案是 return 退出 + context 记录位置(switch 状态机),V8 方案是字节码上下文挂起
第三篇(本文) 自动执行 + async/await Babel 把 async/await 编译为 Generator + 自动执行器;引擎层面各有独立实现

知识图谱

javascript 复制代码
Generator 完整知识图谱
│
├── 核心语法(第一篇)
│   ├── function* 声明
│   ├── yield 暂停 + 产出值
│   ├── next(value) 恢复 + 传参
│   ├── throw(error) 外部抛错
│   ├── return(value) 终止
│   ├── yield* 委托
│   └── Symbol.iterator 协议
│
├── 暂停原理(第二篇)
│   ├── Babel 编译
│   │   ├── switch case 状态机
│   │   ├── context.sent 参数中转站
│   │   ├── try/catch 预计算 catch 入口
│   │   └── regeneratorRuntime
│   └── V8 原生
│       └── 字节码上下文挂起/恢复
│
├── 自动执行 + async/await(第三篇)
│   ├── 自动执行器
│   │   ├── next → Promise → next 递归
│   │   ├── co 库原理
│   │   └── _asyncToGenerator = autoRun 压缩版
│   ├── async/await 编译产物
│   │   ├── async → function*
│   │   ├── await → yield
│   │   └── _asyncToGenerator 包装
│   ├── 关系精确表述
│   │   ├── Babel 层面:语法糖 ✅
│   │   ├── 引擎层面:独立实现 ❌ 不是语法糖
│   │   └── 历史层面:Generator 是 async/await 的"精神前身"
│   └── 高阶玩法
│       ├── 惰性序列(无限数据流 + take/map)
│       └── 并发控制(Promise.race + Promise.allSettled + 限制并发数)
│
└── 面试高频
    ├── async/await 执行顺序(宏任务/微任务)
    ├── return vs done
    ├── yield* 委托返回值
    ├── for...of 不含 return 值
    └── 手写自动执行器

参考


本文所有代码均经过实际运行验证。自动执行器、Babel 编译产物、面试题输出均为真实运行结果。

欢迎关注和评论~

相关推荐
windliang1 小时前
Claude Code 源码分析(六):上下文的发现、注入与压缩
前端·javascript·人工智能
张龙6871 小时前
10 万条数据不卡顿:不定高虚拟列表从原理到生产实现
前端·javascript·性能优化
玉鸯1 小时前
界面用完即消失:Agent 生成式 UI 的短暂性哲学与前端工程的未来
前端·llm·agent
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十四):管理员个人资料页面、管理员日志优化
前端·后端·go
陆枫Larry2 小时前
并发、并行、竞态的区别梳理
前端
水煮白菜王2 小时前
商用地图全面收费?从天地图到开源生态的替代路线
前端·javascript·高德地图·amap·开源地图
程序员黑豆2 小时前
鸿蒙应用开发:网络请求三种方式详解(http / rcp / axios)
前端·harmonyos
黄敬峰2 小时前
前端路由到底是个啥?React Router v7 从零到一,大白话一次讲透
前端·面试