JavaScript Generator 状态机原理
核心矛盾: 开发者想用 async/await 写代码,但旧版 Chrome/Node 不支持。 Babel 的解决方案:把新语法"翻译"成等效的旧语法。 async/await 的底层是 Generator,所以必须先搞清楚 Generator。 普通函数:调用一次,运行到底,返回一个值。 Generator 函数:可以暂停,每次暂停时产出一个值,下次调用时从上次暂停的地方继续执行。 关键: Generator 函数被调用时不会立即执行,而是返回一个迭代器对象(iterator)。 每次调用 .next() 才会执行,直到下一个 yield 或函数结束
官方文档:https://tc39.es/ecma262/#sec-generatorfunction-objects
适用版本:ES2015+(2026-05-08 核实)
理解 Babel 如何把async/await编译成你在混淆代码里看到的case 0: ... case 23:结构最后更新:2026-03-05(经官方文档复核)
一、为什么会有这个东西?
时间线
2009 Node.js 诞生,callback hell 开始肆虐
2012 Promise/A+ 规范发布,链式调用缓解了回调地狱
2015 ES2015 (ES6) 引入 Generator 函数
2016 Facebook 发布 regenerator,把 Generator 编译为 ES5
2017 ES2017 引入 async/await(语法糖,本质是 Generator + Promise)
2018 主流浏览器原生支持 async/await
至今 Babel 在构建时仍将 async/await 编译为 Generator 状态机(兼容旧环境)
核心矛盾: 开发者想用 async/await 写代码,但旧版 Chrome/Node 不支持。
Babel 的解决方案:把新语法"翻译"成等效的旧语法。
参考:
二、先理解 Generator 函数(ES2015)
async/await 的底层是 Generator,所以必须先搞清楚 Generator。
Generator 是什么?
普通函数:调用一次,运行到底,返回一个值。
Generator 函数:可以暂停,每次暂停时产出一个值,下次调用时从上次暂停的地方继续执行。
function* counter() {
console.log('start');
yield 1; // 暂停,产出 1
console.log('resumed');
yield 2; // 暂停,产出 2
console.log('end');
return 3; // 结束,产出 3
}
const gen = counter(); // 不会执行任何代码,只创建迭代器
gen.next(); // 执行到第一个 yield
// 打印: "start"
// 返回: { value: 1, done: false }
gen.next(); // 从上次暂停处继续
// 打印: "resumed"
// 返回: { value: 2, done: false }
gen.next(); // 继续执行到结束
// 打印: "end"
// 返回: { value: 3, done: true }
gen.next(); // 已经结束
// 返回: { value: undefined, done: true }
Generator 怎么实现"暂停"?
关键: Generator 函数被调用时不会立即执行,而是返回一个迭代器对象(iterator)。
每次调用 .next() 才会执行,直到下一个 yield 或函数结束。
可以把 Generator 理解成一个可以暂停的状态机:
状态 0 (初始)
↓ .next()
执行到第一个 yield,暂停
↓ .next()
从暂停处继续,执行到第二个 yield,暂停
↓ .next()
从暂停处继续,执行到 return,结束
参考:
三、async/await 是 Generator + Promise 的语法糖
// async/await 写法
async function fetchUser() {
const resp = await fetch('/api/user');
const data = await resp.json();
return data;
}
等价于(手动展开):
// Generator + Promise 手动实现
function fetchUser() {
return run(function*() {
const resp = yield fetch('/api/user'); // yield 代替 await
const data = yield resp.json();
return data;
});
}
// 驱动函数:自动把 Generator 的 yield 和 Promise 连起来
function run(genFn) {
return new Promise((resolve, reject) => {
const gen = genFn();
function step(nextValue) {
let result;
try {
result = gen.next(nextValue); // 推进 generator
} catch (e) {
return reject(e);
}
if (result.done) {
return resolve(result.value); // generator 跑完了,resolve
}
// generator 还没跑完,等待 yield 出来的 Promise
Promise.resolve(result.value).then(step, reject);
// ↑
// Promise 完成后,把结果传回 generator,继续执行
}
step(undefined); // 启动
});
}
每个 await 本质上是:
yield出一个 Promise,暂停函数执行- 等 Promise resolve 之后,把结果用
.next(result)传回 Generator,继续执行
参考:
四、Babel 如何编译 async/await(regenerator 变换)
Babel 使用 @babel/plugin-transform-async-to-generator + regenerator-runtime 把 async/await 编译为 ES5 兼容的代码。
核心思路:把函数体转换为一个 switch-case 状态机,用一个变量记录当前执行到了哪一步。
一个简单的例子
原始代码:
async function example(url) {
const resp = await fetch(url);
const data = await resp.json();
return data.name;
}
编译后(精简注释版):
function example(url) {
return _asyncToGenerator(function* () {
var resp, data;
return regeneratorRuntime.wrap(function _callee$(ctx) {
while (1) {
switch (ctx.prev = ctx.next) {
case 0:
ctx.next = 2;
return fetch(url); // yield fetch(url),暂停
case 2:
resp = ctx.sent; // 接收 fetch 的结果
ctx.next = 5;
return resp.json(); // yield resp.json(),暂停
case 5:
data = ctx.sent; // 接收 json() 的结果
return ctx.abrupt("return", data.name); // return data.name
case 7:
case "end":
return ctx.stop();
}
}
}, _callee);
})();
}
状态机变量对照表
| 变量名 | 含义 |
|---|---|
ctx.next / e.next |
下一步要执行的 case 编号 |
ctx.prev / e.prev |
上一步执行的 case 编号(用于 try-catch) |
ctx.sent / e.sent |
上一个 yield 的返回值(即 Promise resolve 的值) |
ctx.t0, ctx.t1... |
临时变量(原始代码里的中间结果) |
ctx.abrupt("return", v) |
对应 return v |
ctx.abrupt("break", label) |
对应 break |
ctx.stop() |
函数执行结束 |
return ctx.next=N, expr |
await expr:把 next 设为 N,yield 出 expr |
五、逐行解读你在逆向中遇到的代码
你实际看到的代码(原始)
function N(){
return (N = (0,o.Z)(s().mark(
(function e(t) {
var i,o,a,c,f,d,h,p,v,m,w,x,C,R;
return s().wrap((function(e){
for(;;) switch(e.prev = e.next){
case 0:
return e.next=2, P(t);
case 2:
i=e.sent, o=i.url, ...
c=(0,r.Z)(i,k),
f="string"===typeof t ? t : t.url,
...
case 9:
if(!(0,O.ut)(f)){ e.next=23; break }
return e.next=16, Promise.resolve().then(n.bind(n,1218));
case 16:
m=e.sent, w=m.changeURLOrigin,
x=m.mergeRequestSharedParams,
C=m.mergeRequestSharedHeader,
c.body=x(f,c),
c.headers=C(f,c.headers),
o=w(f,o,c);
case 23:
R=null===c||void 0===c?void 0:c._res,
...
return e.abrupt("return", fetch(o,c).then(...));
case 26:
case "end":
return e.stop()
}
}), e)
})
))).apply(this,arguments)
}
还原后的 async/await 版本(逐步分析)
变量解码:
| 混淆变量 | 实际含义 | 推断依据 |
|---|---|---|
t |
请求参数(URL 字符串或配置对象) | 函数入参 |
i |
await P(t) 的结果(处理后的请求配置) |
case 2: i = e.sent |
o |
最终请求 URL | o = i.url |
c |
请求 config(headers、body 等) | fetch(o, c) |
f |
原始 URL(用于判断接口类型) | f = typeof t === 'string' ? t : t.url |
C |
mergeRequestSharedHeader 函数 |
case 16: C = m.mergeRequestSharedHeader |
R |
响应处理器 | R = c._res |
还原:
async function sendRequest(requestConfig) {
// case 0~2: await P(requestConfig) → 预处理请求(建 headers、转换 URL)
const prepared = await P(requestConfig);
const finalUrl = prepared.url;
const config = omit(prepared, ['url', 'noTextReplace']);
const originalUrl = typeof requestConfig === 'string'
? requestConfig
: requestConfig.url;
// 设置超时(400ms 或自定义)
const timeout = requestConfig?.timeout ?? 400;
let isTimeout = false;
const controller = new AbortController();
const timer = setTimeout(() => { isTimeout = true; controller.abort(); }, timeout);
config.signal = config.signal || controller.signal;
// case 9~23: 如果是特殊接口(/api/mobile, /yewu 等),合并共享参数和 headers
if (isSpecialUrl(originalUrl)) {
// case 9~16: 动态加载共享模块(chunk 1218)
const sharedModule = await import(/* chunk 1218 */ './sharedModule');
const { changeURLOrigin, mergeRequestSharedParams, mergeRequestSharedHeader } = sharedModule;
config.body = mergeRequestSharedParams(originalUrl, config);
config.headers = mergeRequestSharedHeader(originalUrl, config.headers);
finalUrl = changeURLOrigin(originalUrl, finalUrl, config);
}
// case 23: 真正发出请求
const customResponseHandler = config._res;
delete config._res;
return fetch(finalUrl, config) // ← 调用栈指向这里
.then(response => {
if (!response.ok) throw new HttpError({ status: response.status, ... });
// 解析响应...
})
.catch(err => { ... })
.finally(() => clearTimeout(timer));
}
六、状态机的核心模式速查
掌握这几个模式,95% 的编译代码都能读懂:
模式 1:await 表达式
// 原始
const result = await somePromise();
// 编译后
case N:
return e.next = N+2, somePromise(); // yield,暂停
case N+2:
result = e.sent; // 接收结果,继续
模式 2:return 语句
// 原始
return someValue;
// 编译后
return e.abrupt("return", someValue);
模式 3:if 条件(条件为假时跳过)
// 原始
if (condition) {
// block A (cases 10-15)
}
// block B (case 16)
// 编译后
case 9:
if (!condition) { e.next = 16; break } // 条件为假 → 跳到 16
// ... block A 的 cases
case 16:
// block B
模式 4:try-catch
// 原始
try {
const x = await riskyOp();
} catch (err) {
handle(err);
}
// 编译后
case N:
e.prev = N; // 记录 try 块起点(用于找 catch)
return e.next = N+2, riskyOp();
case N+2:
x = e.sent;
e.next = N+6; break; // try 正常结束 → 跳过 catch
case N+4:
e.prev = N+4; // catch 块
err = e.t0;
handle(err);
case N+6:
// 后续代码
模式 5:临时变量 e.t0, e.t1...
// 原始(三元表达式含 await)
const x = condition ? await a() : await b();
// 编译后
case N:
if (!condition) { e.next = N+4; break }
return e.next = N+3, a();
case N+3:
e.t0 = e.sent; e.next = N+6; break;
case N+4:
return e.next = N+6, b();
case N+6:
x = e.t0;
七、s() 和 o.Z 是什么?
你在真实代码里还会看到这些:
// 代码里的
var s = n(87794); // 87794 是 regenerator-runtime
s = n.n(s); // .n() 是 webpack 的 ESM/CJS 互操作包装
return (N = (0, o.Z)(s().mark(function e(t) { ... })))
对照关系:
| 混淆写法 | 原始含义 |
|---|---|
s() |
regeneratorRuntime(全局运行时) |
s().mark(fn) |
regeneratorRuntime.mark(fn):标记一个 generator 函数 |
s().wrap(fn, self) |
regeneratorRuntime.wrap(fn, self):创建状态机执行器 |
o.Z |
asyncToGenerator(Babel 辅助函数) |
(0, o.Z)(...) |
调用 asyncToGenerator,(0, fn)() 是防止 this 绑定的写法 |
完整的包装层次:
asyncToGenerator( ← 把 Generator 包装成 Promise
regeneratorRuntime.mark( ← 标记这是一个 Generator 函数
function*(t) {
return regeneratorRuntime.wrap( ← 创建状态机
function(ctx) {
switch(ctx.prev = ctx.next) {
// 状态机 cases
}
},
_callee ← Generator 函数自身的引用
)
}
)
)
参考:
八、自己动手实验
最好的学习方式是用 Babel REPL 自己编译,观察变化。
方法 A:Babel 在线 REPL
左侧 Presets 勾选 env,然后粘贴:
async function example(url) {
const resp = await fetch(url);
const data = await resp.json();
if (data.ok) {
const extra = await fetchExtra();
return { data, extra };
}
return { data };
}
右侧立刻看到编译结果,对照本文档理解每一行。
方法 B:本地编译
# 安装
npm install --save-dev @babel/core @babel/plugin-transform-async-to-generator \
@babel/plugin-transform-regenerator \
regenerator-runtime
# 新建 .babelrc
echo '{"plugins": ["@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator"]}' > .babelrc
# 编译
npx babel input.js -o output.js
方法 C:Chrome DevTools 实时观察
- 在控制台定义一个 async 函数
- 打断点,调用它
- 在 Sources 面板的 Scope 区域观察
ctx/e对象的值变化
九、总结
你写的: async function f() { const x = await p(); return x; }
↓ Babel 编译
Generator 函数: function* f() { const x = yield p(); return x; }
↓ regenerator-runtime 变换
状态机: switch(ctx.next) {
case 0: return ctx.next=2, p();
case 2: x = ctx.sent; return ctx.abrupt("return", x);
}
↓ 你在逆向里看到的
混淆版: switch(e.prev=e.next){case 0:return e.next=2,p(t);
case 2:i=e.sent;return e.abrupt("return",i)}
读懂的关键只有三条:
return e.next=N, expr=await expr(暂停,下一步去 case N)e.sent= 上一个 await 的结果e.abrupt("return", v)=return v
最佳实践
识别状态机的关键特征是 switch(e.prev = e.next):Babel 编译的 async 函数基本结构固定,e.prev(上一个 case)和 e.next(下一个目标 case)是状态机跳转的核心;看到这对变量可以快速确认是编译后的 Generator。
通过 case 数字还原 await 结构:每个 await 对应两个相邻 case:第一个 case 触发异步操作并设置 e.next,下一个 case 接收结果(e.sent)。数一数有多少对 case 就知道有多少个 await。
逆向时优先找 _asyncToGenerator 或 regeneratorRuntime:这两个是 Babel 编译 async 函数的运行时辅助函数,全局搜索这些关键字可以快速定位文件中所有的 async 函数入口。
e.abrupt("return", v) 等价于 return v:Babel 用 e.abrupt 系列方法替代了 return/break/continue;看到这些方法调用时直接在脑中翻译为对应的控制流关键字。
用 babel 反编译验证理解:对混淆代码的某段状态机,用 @babel/parser 解析后用 @babel/plugin-transform-async-to-generator 的逆过程(或直接用新版 Babel 编译等价的 async 代码),对比结构确认是否理解正确。
常见陷阱
陷阱:e.sent 是 undefined,以为是 await 没有返回值
现象: 在断点中看到 e.sent 是 undefined,以为被 await 的 Promise 没有 resolve 值。
原因: e.sent 在第一个 case(触发异步操作时)确实是 undefined;需要在设置了 return 语句的 case 执行之后,下一个 case 开始时才能拿到 e.sent 的值。
解决: 断点设在 e.next = X 之后的 case X: 开头,此时 e.sent 才是 await 的结果。
陷阱:把 _asyncToGenerator 的闭包层误认为业务代码
现象: 进入 async 函数后堆栈出现多层函数(_asyncToGenerator、_callee$、step),不知道哪一层是实际业务逻辑。
原因: Babel 把每个 async 函数包裹在 _asyncToGenerator(function* _callee$() {...}) 中,_asyncToGenerator 负责运行 Generator,_callee$ 才是实际逻辑。
解决: 直接在 Call Stack 中点击 _callee$ 函数帧,跳到实际业务逻辑层。
陷阱:e.abrupt("throw", err) 混淆了 return 和 throw
现象: 看到 e.abrupt("return", ...) 以为函数 return,实际上是 e.abrupt("throw", ...) 抛出了异常。
原因: Babel 将 throw 编译为 e.abrupt("throw", err),try/catch 对应 e.prev = try_start, e.stop = catch_start;字面量相似,不仔细看容易混淆。
解决: 记忆区分:"return" = 函数返回;"throw" = 抛出异常;"break" = 循环 break;"continue" = continue。