XState — 把状态画成图,让矛盾写不出来
已复核XState 是一个 JavaScript/TypeScript 状态机与 statechart 运行时。日常类比:地铁线路图是纯数据,列车才是正在跑的进程。createMachine() 画出站点与允许的换乘;createActor(machine) 再把图跑起来。
你写:
import { createMachine, createActor } from "xstate";
const fetchMachine = createMachine({ id: "fetch", initial: "idle", states: { idle: { on: { FETCH: "loading" } }, loading: { on: { OK: "success", FAIL: "error" } }, success: {}, error: { on: { RETRY: "loading" } } }});
const actor = createActor(fetchMachine);actor.start();actor.send({ type: "FETCH" });固定 5.32.6 里,createMachine 返回 StateMachine(实现 ActorLogic),createActor 返回带 mailbox 的 Actor。interpret 只是 createActor 的弃用别名。
不理解 XState,下面这些事都没法解释:
- 为什么多个布尔标志会交叉出大量非法组合,而状态图从源头删掉未画出的边
- 为什么
send不是同步改当前状态,而是先入队再串行transition - 为什么
setup().createMachine()和直接createMachine()运行时都能建机器,但类型推断路径不同 - 为什么 React 绑定要订阅 snapshot,而不是把 ActorRef 当成 machine 传进 hook
固定版本的主链可以拆成五步:
-
描述机器:
createMachine(config)或setup({ actors, actions, guards, delays }).createMachine(config)构造StateMachine。setup的第二职责是把实现表和类型绑在一起;直接createMachine的第二参数 implementations 在源码注释里标为 DEPRECATED,应改用setup或machine.provide()。 -
创建 Actor:
createActor(logic)隐式建一个以自己为根的 actor system。构造时就调用logic.getInitialSnapshot(),所以未start()也能getSnapshot()。 -
启动并打开 mailbox:
start()把状态标为 Running,执行logic.start,通知订阅者,再mailbox.start()。启动前入队的事件会在此刻冲洗。 -
串行转移:
send(event)经system._relay进入_send,mailbox 一次只_process一个事件。StateMachine.transition跑完整macrostep;当前状态没有匹配边时,microstep直接返回原 snapshot。 -
快照与子 actor:观察者拿到的是 snapshot(含
value/context/status),snapshot.matches('loading')比较的是value。assign用Object.assign({}, oldContext, partial)产出新 context,再cloneMachineSnapshot。子 actor 复用父 system,不能对自己调用stop()。
案例 1:未画出的事件不会改状态
Section titled “案例 1:未画出的事件不会改状态”actor.send({ type: "LOGOUT" }); // 当前若在 loading,selectTransitions 为空console.log(actor.getSnapshot().value); // 仍是 loading空转移不是“丢进黑洞就没进运行时”:事件仍入队并走完 _process,只是 microstep([]) 返回原 snapshot。snapshot.can(event) 可以事先问有没有非 forbidden 转移。
案例 2:React 里用 useActor,不要把 ActorRef 再塞回去
Section titled “案例 2:React 里用 useActor,不要把 ActorRef 再塞回去”import { useActor } from "@xstate/react";
function FetchView() { const [state, send] = useActor(fetchMachine); if (state.matches("loading")) return <p>加载中</p>; if (state.matches("error")) { return <button onClick={() => send({ type: "RETRY" })}>重试</button>; } return <button onClick={() => send({ type: "FETCH" })}>加载</button>;}固定 @xstate/react@6.1.0 里 useMachine 只是 useActor 的弃用别名,返回 [snapshot, send, actor]。useActor 用 useSyncExternalStore 订阅,并在 useEffect 里 start()。若把已经在跑的 ActorRef 传进去,开发模式会抛错,应改用 useSelector(actorRef, ...)。
案例 3:层级状态用绝对 id
Section titled “案例 3:层级状态用绝对 id”const auth = createMachine({ id: "auth", initial: "anonymous", states: { anonymous: { on: { LOGIN: "verifying" } }, verifying: { initial: "password", states: { password: { on: { OK: "mfa", FAIL: "#auth.anonymous" } }, mfa: { on: { OK: "#auth.authenticated" } } } }, authenticated: { on: { LOGOUT: "anonymous" } } }});# 是状态 id 前缀。FAIL: '#auth.anonymous' 从任意子状态跳回登录,不必手写一层层 if。
-
把字符串当事件:开发构建里
send("FETCH")会抛错,必须send({ type: "FETCH" })。 -
以为没 start 就读不到 snapshot:构造函数已经
getInitialSnapshot。真正的边界是 mailbox 尚未start(),启动前的send会排队,不会立刻转移。 -
把
useMachine当成与useActor不同的两套 API:固定 React 包里前者只是别名;把 ActorRef 传给useActor会在开发模式失败。 -
闭包里捏住旧 context:
assign每次浅合并出新对象。const c = snapshot.context之后再 send,c不会跟着变。 -
对子 actor 直接
stop():非根 actor 会抛A non-root actor cannot be stopped directly;应让父机器用stopChild或走到终态。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 认证、向导、播放器、支付这类状态多、非法组合代价高的流程
- 需要同一份机器跨 React / Vue / Svelte adapter 复用
- 需要把转移图画给非工程师看,或把 snapshot 持久化后再
restoreSnapshot
不适用:
- 只有两三个线性状态的小表单——状态图元数据比收益大
- 需要隐式依赖收集、直接
state.count++的编辑器模型——见 mobx - 不能接受固定 5.32.6 的 actor / mailbox 合同,或准备直接跟 v6 alpha 走
固定版本边界
Section titled “固定版本边界”- 本文绑定
statelyai/xstate@21872cdc...,packages/core的 package 为xstate@5.32.6。npm 未暴露gitHead,锚点是 GitHub tagxstate@5.32.6。 - 同提交里
@xstate/react为6.1.0;useMachine已标 deprecated。 - 仓库另有
xstate@6.0.0-alpha.*tag,本文不绑定 alpha。 - 本文未安装依赖、运行上游测试、浏览器渲染或 bundle 测量,状态保持
UNVERIFIED。
- 机器是逻辑,Actor 才是进程——同一张图可以多实例、可持久化、可检查。
- mailbox 把并发 send 收成串行 macrostep——转移原子性来自队列,不是来自“状态对象自己上锁”。
- 未画出的边不是运行时魔法忽略——事件仍被处理,只是空转移列表让 snapshot 保持原样。
- 类型入口和运行时入口可以分开——
setup()主要服务实现表与推断;运行时仍是StateMachine+Actor。
- 在
loading且机器没有LOGOUT边时send({ type: "LOGOUT" }),snapshot.value 会变成什么? createActor(machine)之后、start()之前,getSnapshot()会不会因为“还没 start”而得到undefined?- 固定
@xstate/react@6.1.0里,把已经start()的 ActorRef 传给useActor,开发模式会怎样?
检查点:
- 仍是
loading;空转移返回原 snapshot。 - 不会。构造时已写入初始 snapshot;未 start 只意味着 mailbox 还没冲洗。
- 会抛错,提示改用
useSelector(actorRef, ...)。
- 文档:stately.ai/docs
- 固定源码:statelyai/xstate —— 本文绑定提交
21872cdc93a3baddbcf43f1d83553991d39f28ab - 标准:W3C SCXML 1.0
- mobx —— 隐式依赖收集的互补路线
- effect —— 更偏代数效应与 typed concurrency 的 actor 亲戚
- mobx —— 反应式 store;XState 显式画边,MobX 隐式收集读依赖
- react ——
@xstate/react用useSyncExternalStore订 snapshot - effect —— FP 侧重的并发与资源作用域
- tanstack-query —— 服务端请求状态机,不替代本地流程状态图