JavaScript异步编程进化史:从Generator到Async/Await的底层原理
深入解析Generator函数的运行时状态机、上下文管理、yield*委托机制,揭示async/await如何从Generator+Promise模式演进而来,助你从容编写和调试异步代码。
JavaScript 异步编程:从 Generator 到 Async/Await 的演进
写 JavaScript 的人,早晚会遇到异步操作。回调地狱、Promise 链、Generator 函数、再到现在的 async/await,这条路走了好几年。今天聊聊这段历史里一个关键角色——Generator 函数,以及它背后的运行时机制。
Generator 函数是什么
Generator 是 ES6 引入的一种特殊函数。普通函数跑到底,Generator 可以中途暂停,再接着跑。这种“暂停-恢复”的能力,让它成了异步编程的利器。
写一个 Generator 很简单:
function* myGen() {
yield '第一步';
yield '第二步';
return '结束';
}
调用它不会立刻执行,而是返回一个迭代器对象。每次调用 .next(),函数就跑到下一个 yield 停下,把值吐出来。
Generator 背后的运行时
Generator 能暂停恢复,靠的不是 JavaScript 引擎的特殊魔法,而是一套精心设计的运行时状态机。这套机制在编译后的代码里能看到痕迹。
运行时维护了几个关键状态:
- suspendedStart:刚创建,还没开始跑
- suspendedYield:在 yield 处暂停了
- executing:正在执行中
- completed:跑完了
每次调用 .next(),运行时检查当前状态,决定下一步怎么走。如果正在执行,会抛错——Generator 不允许重入。
上下文管理:try-catch-finally 的实现
Generator 里可以用 try-catch-finally,这需要运行时额外维护一个“上下文”对象。上下文里有个 tryEntries 数组,记录每个 try 块的位置信息。
比如:
function* demo() {
try {
yield 1;
} catch (e) {
console.log('捕获:', e);
} finally {
console.log('清理');
}
}
运行时把 try 块的起始位置、catch 块位置、finally 块位置都存下来。执行到 yield 时暂停,下次恢复时根据当前位置决定跳转到哪一块。
委托机制:yield* 的底层实现
Generator 可以嵌套,用 yield* 把执行权交给另一个 Generator。这个功能叫“委托”。
运行时遇到 yield*,会创建一个委托对象,记录被委托的迭代器。每次 .next() 调用,先检查有没有委托。如果有,就调用被委托迭代器的对应方法。
如果被委托的迭代器没有某个方法(比如 throw),运行时自动回退到主 Generator 的对应逻辑。这种设计保证了嵌套调用的健壮性。
从 Generator 到 Async/Await
Generator 本身不是为异步设计的,但它提供了暂停恢复的能力。聪明的开发者发现,可以用 Generator 配合 Promise,写出像同步代码一样的异步逻辑。
早期的做法是写一个“执行器”:
function run(gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (!result.done) {
result.value.then(next);
}
}
next();
}
这个模式后来被标准化成了 async/await。本质上,async 函数就是 Generator 加上自动执行器,await 就是 yield 的语法糖。
实际开发中的选择
现在写新代码,基本都用 async/await。它更简洁,错误处理更自然。但理解 Generator 的原理仍然有价值:
- 调试复杂异步流程时,知道底层怎么跑,能更快定位问题
- 写工具库或框架时,可能需要手动控制执行流程
- 一些老项目还在用 Generator + co 库的方式
Generator 的运行时实现,展示了 JavaScript 语言设计的一个侧面:用简单的机制组合出强大的功能。状态机、委托、上下文管理,这些概念在其他语言里也有,但 JavaScript 用自己特有的方式把它们揉在了一起。
性能与注意事项
Generator 函数每次 .next() 调用都有开销。运行时需要创建上下文、检查状态、处理委托。高频调用场景下,性能可能不如普通函数。
另外要注意,Generator 不是线程。JavaScript 是单线程的,Generator 的暂停只是让出了执行权,不是真正的并发。它和事件循环配合,实现了“看起来像同步”的异步效果。
如果 Generator 内部抛出异常,外部没有 try-catch 的话,异常会沿着调用链往上冒。用 .throw() 方法可以从外部向 Generator 内部注入异常,这在某些调试场景下有用。
总结
Generator 函数是 JavaScript 异步编程进化史上的重要一站。它引入了暂停恢复的语义,为后来的 async/await 铺平了道路。理解它的运行时机制,能帮你更深入地掌握 JavaScript 的执行模型。
写代码的时候,用 async/await 就好。但心里清楚,这层语法糖底下,是 Generator 和 Promise 在默默工作。

