为什么 async/await 杀不死 Generator?

最近在做代码 Review 时,被一段处理 Policy(政策)批处理的逻辑勾起了很多思考。

1. 缘起:一次“失败”的逻辑抽取

业务场景其实很常见:分页拉取数据、做 24h 业务过滤、统计成功/跳过/报错的数量,最后每 10 个一组聚合发送。

起初,大家习惯性地想用 async/await 来写。但当你试图把“分页 + 过滤”这部分通用的数据获取逻辑抽出来复用时,你会发现代码变得极其扭曲。最常见的尝试是写一个带有 Callback(回调) 的工具函数:

JavaScript

// 这种模式下,控制权在 walkPolicies 手里 await walkPolicies\(async \(data\) => \{ stats\.success\+\+; buffer\.push\(data\); if \(buffer\.length === 10\) await sendBatch\(buffer\.splice\(0, 10\)\); \}, \(\) => stats\.skip\+\+\);

很多人说 Generator 的好处是“关注点分离”,但其实回调函数也能做到。真正的痛点在于“非侵入性”。 在回调模式下,如果你想改动执行节奏(比如发完一组歇一会儿),或者想中途停止,你必须去修改工具函数的内部实现

而 Generator 的价值在于:它允许你在不触碰执行函数内部逻辑的前提下,由外部决定如何、何时推进流程。


2. 控制权的反转:不改源码的解耦

换成 async generator 后,生产者和消费者的界限变得极其清晰。

生产者(只管业务流,不关心频率):

1
2
3
4
5
6
7
8
9
10
11
12
13
async function* policyProvider() {
let page = 1;
while (true) {
const batch = await fetchPage(page++);
for (const policy of batch) {
if (await wasSeenIn24Hours(policy.holderId)) {
yield { type: 'SKIP' };
} else {
yield { type: 'DATA', payload: policy };
}
}
}
}

消费者(掌控一切):

1
2
3
4
5
6
7
8
for await (const result of policyProvider()) {
// 所有的节奏控制、统计、批处理逻辑都在这里
// 我不需要去改 policyProvider 的代码,就能实现各种复杂的调度
if (result.type === 'DATA') {
buffer.push(result.payload);
if (buffer.length === 10) await sendBatch(buffer.splice(0, 10));
}
}

这种 控制反转(IoC) 最大的价值是:生产者只负责产出状态,而外部可以根据状态控制生成者。这种“非侵入式”的调度,是 async/await 很难优雅实现的。


3. 底层硬核:为什么我们会转入原理?

当我们发现 async/await 无法优雅地处理这种逻辑抽取时,直觉会把我们带向底层:Generator 和普通的函数到底差在哪?

这涉及到 JS 引擎对内存的魔术。普通函数高度依赖物理调用栈(Stack),执行完即销毁。但 Generator 彻底打破了栈的物理限制:

  • 暂停(Yield):引擎会把当前的执行上下文(变量、IP 指针、this 等)直接从物理栈里弹出来,**写回到堆(Heap)**里的对象中封存。

  • 恢复(Next):引擎从堆里把这个快照捡回来,重新压回栈顶接着跑。

而且,Normal Generator 的暂停和恢复是同步的。它不需要排队,它就是在同一个执行序列里完成了“现场切换”。

所以,闭包让“作用域”活了下来,而 Generator 让整个“执行过程”活了下来。


4. 写在最后:为什么它成了“少数派”的武器?

既然 Generator 这么强大,为什么现在很少见到了?

第一,它的“双向通信”反而是个负担。

Generator 允许通过 next\(val\) 往函数内部传值,这在理论上很美,但在业务代码中极其少见。这种复杂的交互让代码的可读性直线下降,如果只是为了拿数据,async/await 配合简单的循环显然更自然。

第二,它处于一种“不上不下”的尴尬位置。

在 90% 的简单场景下,我们只需要“等一个结果”,async/await 的心智负担最低。在极致复杂的响应式场景下,大家又去用 RxJS 了。

但这并不代表它过时了。当你发现 async/await 让你不得不通过修改函数内部来实现外部控制时,说明你需要的不再是一个简单的异步结果,而是一个**“状态保存在堆里的执行环境”**。


评论