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

系列前两篇我们讲了:
- 第一篇:Generator 概念、yield/next/throw/return、Iterator 协议
- 第二篇:Babel 编译产物剖析、switch 状态机、V8 引擎层面的暂停机制
第一篇结尾留了一个问题:async/await 和 Generator 到底什么关系? 当时说了一句"async/await 在引擎层面有自己独立的实现,并不是 Generator 的语法糖"------但没展开。
这篇就来展开。我们从三个角度切入:
- 自动执行器 ------手动调
next()太痛苦,怎么自动执行? - Babel 编译产物------async/await 被 Babel 编译后变成了什么?
- 高阶玩法 + 面试题------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());
};
}
逻辑拆解:
- 调
gen.next()拿到{ value, done } - 如果
done为 true,Generator 结束,value就是最终结果 - 如果
done为 false,value通常是一个 Promise,等它 resolve - resolve 后拿值调
gen.next(val)------回到步骤 1 - 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 function → function*
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后面的同步代码报错**。举个例子:
jsasync 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),但逻辑就是:
- 调
next()或throw() - 拿到
{ value, done } - done → 结束
- !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不会被转成 Generatorawait不会被转成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
script start→ 立即输出async1()调用 → 进入async1,async1 start立即输出- 调用
async2()→ 函数体同步执行 ,async2立即输出(async2()本身是同步调用) await触发:后面的代码(async1 end)被放入微任务队列- 继续执行同步代码:
promise立即输出,resolve()调用后.then回调也进入微任务队列 script end输出
然后清空微任务队列:async1 end → then(两者都是微任务,谁先入队谁先出)。最后宏任务 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 的关系,精确表述
答: 三个层面:
- Babel 编译层面 :async/await 被编译为 Generator +
_asyncToGenerator(自动执行器),确实是"语法糖" - 原生引擎层面:async/await 有独立的语义和字节码实现,基于 Promise 微任务调度,不是 Generator 的语法糖
- 历史演进层面: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 官方:plugin-transform-async-to-generator --- async/await → Generator 编译插件
- Babel 官方:plugin-transform-async-generator-functions --- ES2018 async iteration
- Babel helpers:_asyncToGenerator 源码 --- 自动执行器实现
- Babel helpers:regeneratorRuntime --- Generator 编译产物运行时
- ECMAScript 规范:async function 语义 --- 官方语义定义
- V8 博客:ES6 引擎实现 --- V8 原生 Generator 实现
- co 库源码 --- TJ Hollowaychuk 的 Generator 自动执行库
本文所有代码均经过实际运行验证。自动执行器、Babel 编译产物、面试题输出均为真实运行结果。
欢迎关注和评论~