在 JavaScript 中,状态机常用于描述对象在不同阶段之间的流转,例如订单处理、游戏角色行为或协议解析。Generator 函数提供了用同步写法表达异步等待与状态暂停的能力,配合 yield 关键字,可以把复杂的状态切换逻辑拆解为线性代码,而不必维护庞大的条件分支表。

一、Generator 与状态机的基础原理
Generator 函数通过 function* 声明,调用后并不立即执行函数体,而是返回一个迭代器对象。每次调用迭代器的 next() 方法,函数会运行到下一个 yield 表达式处暂停,并把 yield 后面的值作为返回结果的 value 传出。这种“可暂停、可恢复”的特性,恰好对应状态机中“进入某状态、等待事件、转移到下一状态”的过程。
在传统的 switch 状态机中,我们通常用变量保存当前状态,再根据事件做分支判断。而使用 Generator,每一个 yield 点就自然成为一个状态停留点,函数内部的执行上下文(局部变量、执行位置)由引擎自动保存,无需手动维护状态变量。这使得状态逻辑可以用接近自然语言的顺序结构来表达。
1.1 最简状态机示例
下面用一个简单的红绿灯状态机展示基本写法。每次 next 调用推进一个状态,yield 返回当前状态名称。
function* trafficLight() {
while (true) {
yield '红灯';
yield '绿灯';
yield '黄灯';
}
}
const light = trafficLight();
console.log(light.next().value); // 红灯
console.log(light.next().value); // 绿灯
console.log(light.next().value); // 黄灯
console.log(light.next().value); // 红灯
这段代码中,yield 充当了状态输出点,while 循环让状态无限轮转。虽然简单,但已经体现了 Generator 状态机的核心:状态隐藏在函数的暂停位置中,而不是显式存储在外部变量里。
二、用 yield 接收外部输入实现复杂转移
复杂状态机往往需要根据外部事件决定下一个状态,而不是固定顺序轮转。Generator 的 next(arg) 方法可以接收参数,该参数会成为上一个 yield 表达式的返回值,从而让状态函数读取外部事件并分支处理。
例如订单状态机,包含待支付、已支付、发货、完成、取消等状态,转移依赖用户操作或系统通知。我们可以让每个 yield 返回当前状态,并通过 next 传入触发事件,函数内用 if 或 switch 决定下一步流向。这样状态流转条件集中在函数体内,比分散的回调或事件监听更易于追踪。
2.1 订单状态机实现
以下示例实现了一个支持取消与支付的订单状态机。初始为待支付,收到 pay 事件进入已支付,收到 cancel 进入取消;已支付后收到 ship 进入发货,发货后收到 confirm 进入完成。
function* orderMachine() {
let state = '待支付';
while (true) {
const event = yield state;
if (state === '待支付') {
if (event === 'pay') {
state = '已支付';
} else if (event === 'cancel') {
state = '已取消';
yield state;
return '结束';
}
} else if (state === '已支付') {
if (event === 'ship') {
state = '已发货';
}
} else if (state === '已发货') {
if (event === 'confirm') {
state = '已完成';
yield state;
return '结束';
}
}
}
}
const order = orderMachine();
console.log(order.next().value); // 待支付
console.log(order.next('pay').value); // 已支付
console.log(order.next('ship').value); // 已发货
console.log(order.next('confirm').value); // 已完成
在这个例子中,yield state 把当前状态暴露给外部,外部通过 next 传入事件驱动转移。当进入终态时,我们用 return 结束迭代器,后续调用 next 将返回 done 为 true。这种模式比在对象上挂载 onEvent 方法更紧凑,也避免了状态变量被外部误改。
2.2 状态转移表对比
为了更直观看到 Generator 写法的优势,我们把同等逻辑的传统查表法并列比较:
| 实现方式 | 状态存储 | 转移逻辑位置 | 可读性 |
|---|---|---|---|
| 查表法 | 外部变量或对象属性 | 独立映射表 | 分支多时较难跟踪 |
| Generator 法 | 函数执行上下文 | 函数体内顺序代码 | 接近自然流程描述 |
查表法在状态极多、转移规则频繁变动时便于集中配置,但调试时要同时看表和处理器。Generator 法把“状态是什么”和“收到什么事件后去哪”写在一起,新人阅读时更容易建立完整心智模型。
三、在异步场景中的状态机应用
Generator 天生适合配合 Thunk 或 Promise 实现异步流程控制。复杂状态机若某些状态需要等待接口返回,可在 yield 处产出 Promise,由外部驱动器 resolve 后再 next 传回结果,从而写出“看起来同步”的异步状态代码。
比如支付状态机中,已支付后需要调用物流接口获取运单号,再进入发货状态。我们可以 yield 一个返回 Promise 的函数,驱动器 await 完成后把运单号传入 next,状态机继续往下走。这样避免了回调嵌套,也把异步等待点明确标记在状态流中。
3.1 异步等待示例
下面模拟一个带异步调用的状态机,使用一个简单的 run 函数驱动 Promise 类型的 yield 值。
function getTracking() {
return new Promise(resolve => {
setTimeout(() => resolve('SF123456'), 500);
});
}
function* asyncOrder() {
yield '已支付';
const tracking = yield getTracking();
console.log('运单号:', tracking);
yield '已发货';
}
function run(gen) {
const it = gen();
function step(data) {
const result = it.next(data);
if (result.done) return;
if (result.value instanceof Promise) {
result.value.then(res => step(res));
} else {
console.log('状态:', result.value);
step();
}
}
step();
}
run(asyncOrder);
上述代码中,yield getTracking() 产出 Promise,run 函数检测到后等待其完成,再把结果通过 next 送回赋值给 tracking。整个过程没有嵌套回调,状态顺序一目了然。实际项目中可替换为 co 库或自定义调度器来处理异常与并发。
四、局限与注意事项
Generator 状态机并非银弹。首先,迭代器只能单向推进,无法回到之前的状态,若业务需要回退则要重置迭代器或另写反向逻辑。其次,状态数据保存在闭包局部变量中,外部不能直接读取或修改,必须依靠 yield 和 next 通信,这既是封装优势也是调试门槛。
另外,在浏览器或 Node 的旧运行环境中,需注意 Generator 已被广泛支持,但调试堆栈不如普通函数直观。若状态机需要和可视化工具集成,查表法可能更容易序列化和展示。因此选择方案时应权衡团队熟悉度与维护成本,而非盲目追求写法新颖。
4.1 错误传递
Generator 内部抛出的异常可通过迭代器的 throw 方法在暂停点注入,也可以在 next 外捕获。建议在驱动器里统一 catch,避免状态机静默失败。
function* safeMachine() {
try {
yield '运行';
throw new Error('状态异常');
} catch (e) {
yield '异常恢复';
}
}
const m = safeMachine();
console.log(m.next().value);
console.log(m.next().value); // 触发异常并被捕获
console.log(m.next().value);
通过 try-catch 结合 yield,我们能让状态机在出错时切换到恢复状态,而不是整个迭代器终止。这种结构在长生命周期的设备控制逻辑中尤为实用。
五、总结
使用 Generator 与 yield 实现复杂状态机,本质是把控制流暂停能力转化为状态停留点。它用顺序代码替代分支表,用局部变量替代外部状态存储,在可读性上优势明显,并天然适配异步等待。只要清楚其单向性、闭包封装与调试特点,就能在订单流转、协议解析、游戏 AI 等场景中写出更简洁可靠的状态逻辑。