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 本质上是:

  1. yield 出一个 Promise,暂停函数执行
  2. 等 Promise resolve 之后,把结果用 .next(result) 传回 Generator,继续执行

参考:


四、Babel 如何编译 async/await(regenerator 变换)

Babel 使用 @babel/plugin-transform-async-to-generator + regenerator-runtimeasync/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

网址:https://babeljs.io/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 实时观察

  1. 在控制台定义一个 async 函数
  2. 打断点,调用它
  3. 在 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)}

读懂的关键只有三条:

  1. return e.next=N, expr = await expr(暂停,下一步去 case N)
  2. e.sent = 上一个 await 的结果
  3. 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。

逆向时优先找 _asyncToGeneratorregeneratorRuntime:这两个是 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.sentundefined,以为是 await 没有返回值

现象: 在断点中看到 e.sentundefined,以为被 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。


参见

JavaScript Promise 完全指南
混淆还原
Babel AST入门

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog