原语阶梯第三级——Actor。
createMachineActor是「消息驱动的 FSM + 可选 Runner + per-state 消息处理器」。它是SegmentLoaderActor和SourceBufferActor(39、40 篇)的地基。这篇拆它的机制:快照即信号、状态表分发、getContext 的 live-read 语义、onSettled 自动转换。
先看共享底座:machine.ts(47 行)
Actor 和 Reactor(下篇)共享一个底座。machine.ts:40-47 的 createMachineCore:
export function createMachineCore<FullState, Snapshot>(initialSnapshot) {
const snapshotSignal = signal(initialSnapshot); // L43
const getState = (): FullState => untrack(() => snapshotSignal.get().value); // L44
const transition = (to: FullState): void =>
update(snapshotSignal, (current) => ({ ...current, value: to })); // L45
return { snapshotSignal, getState, transition };
}
三件事浓缩了整个机器模型:
- 快照就是一个信号(L43)——状态变化天然响应式,订阅者自动感知。
- getState 用 untrack 读(L44)——在 Computed/Effect 里查状态不建立依赖,避免意外订阅。
- transition 用 update 的函数形式(L45)——
{ ...current, value: to }展开保留其他字段(Actor 的 context),只替换value。28 篇说的「update 是机器机制的基石」就在这。
对外接口 Machine(L24-27):{ readonly snapshot: ReadonlySignal<Snapshot>; destroy(): void }——快照以只读信号暴露,外部拿不到 set,写入只经内部 transition。
ActorSnapshot(actor.ts:16-18)在 MachineSnapshot(只有 value)基础上加 context: Context——文件头注释(L4-7)说明这是镜像 XState 的 snapshot 模型:snapshot.value 是有穷操作模式,snapshot.context 是任意非有穷数据。
ActorDefinition:声明一台机器
create-machine-actor.ts 的定义类型(L78-101):
const definition = {
initial: 'idle', // L93 — 初始状态
context: { /* 任意数据 */ }, // L95
runner: () => new SerialRunner(), // L91 — 可选,创建时调用一次
states: { // L100 — 状态表!
idle: {
on: {
start: (message, ctx) => { ctx.transition('loading'); },
},
onSettled: 'idleAgain', // L62 — runner 空了自动转
},
// ...
},
};
几个关键设计:
on是按消息类型索引的 mapped type(L64-69):{ [M in Message as M['type']]?: (message: Extract<Message, {type: M['type']}>, ctx) => void }——handler 参数用Extract自动收窄到具体消息形状。发{type: 'append', uri: '...'}时,handler 的 message 参数精确是这个形状。runner是工厂(L91),在createMachineActor()时调用一次、随 actor 终生存活(不每状态重建)。'destroyed'是隐式终态——L164 的type FullState = UserState | 'destroyed'框架自动并入,文档(L74-77)要求用户不要自己声明。
HandlerContext:context 的双读语义(L29-45)
这是 Actor 最微妙也最重要的设计:
export interface HandlerContext<UserState, Context, RunnerFactory> {
transition: (to: UserState) => void; // L34
context: Context; // L36 — dispatch 时刻的快照
getContext: () => Context; // L43 — live 读
setContext: (next: Context) => void; // L44
// runner(有工厂时才有,L45 条件交叉)
}
context vs getContext() 的区别(L35-42 注释详述):
context——dispatch 时刻捕获的快照。handler 内 setContext 后即陈旧。getContext()——每次调用重新 untracked 读信号。live 读。
为什么需要两种?看场景:
on: {
append: (msg, ctx) => {
ctx.setContext({ ...ctx.context, queue: [...ctx.context.queue, msg] }); // 快照够用
ctx.runner.schedule(new Task(async (signal) => {
// 异步闭包里,dispatch 时刻的 ctx.context 可能已陈旧
const latest = ctx.getContext(); // ← live 读,拿到「上一个 task 提交后」的 context
await doAppend(latest, signal);
ctx.setContext({ ...latest, done: true });
}));
},
}
串行 task 链中,每个 task 要读「上一个 task 提交后的 context」,而不是 dispatch 时的陈旧快照。getContext()(实现在 L172:untrack(() => snapshotSignal.get().context))提供这个语义。
setContext(L174-176)用 update 的 Partial 形式:update(snapshotSignal, { context })——只换 context,state value 保留。和 transition(换 value 保 context)正好互补。
send():查表分发(L183-210)
send(message) {
const state = getState(); // L184
if (state === 'destroyed') return; // L185 — 防线一
const stateDef = def.states[state as UserState]; // L186
const handler = stateDef?.on?.[message.type]; // L187-189 — 两级查表
if (!handler) return; // L190 — 无 handler 静默丢弃
handler(message, { context: getContext(), getContext, transition, setContext, ... }); // L191-197
// onSettled 注册(L198-208)
}
流程就四步:读当前状态 → destroyed 短路 → 两级查表(状态表 → 消息表)→ 调 handler。
post-destroy send 是 no-op 的双保险(我觉得这个细节很讲究):L185 显式检查是一道;而且 'destroyed' 不在 def.states 里,L186-190 本来也查不到 handler。两道防线,一道语义明确,一道结构兜底。
无 handler 的消息静默丢弃(L190,L63 注释)——不是错误。状态机「这个状态下不认识这条消息」是正常语义(idle 状态收到 progress 消息,忽略)。
onSettled:批次完成后自动转换(L198-208)
// handler 返回之后注册(L198 注释:"Register onSettled after the handler so we read the post-transition state")
const newState = getState(); // L199 — 转换后的状态
const newStateDef = def.states[newState];
if (newStateDef?.onSettled && runner) {
runner.whenSettled(() => {
if (getState() !== newState) return; // L205 — 状态又变了?放弃
transition(newStateDef.onSettled); // L206
});
}
两层防护:注册时机在 handler 之后(读的是转换后的状态);回调里再验 getState() !== newState(注册后状态又被别的消息改掉就放弃)。加上 Runner 内部的引用身份丢弃(30 篇),三重防陈旧。
典型用法:loading 状态的 onSettled: 'idle'——所有加载任务跑完,自动回 idle。
destroy():单步同步(L212-216)
destroy() {
if (/* destroyed */) return; // 幂等
runner?.destroy(); // L214 — 先停 runner
transition('destroyed'); // L215 — 再转终态
}
单步同步。对比 Reactor 的两步销毁(下篇)——Actor 没有 effect 需要异步清理,destroy 就是 runner 停 + 状态标 destroyed。
createTransitionActor:纯 reducer 形态(76 行)
还有个轻量兄弟 createTransitionActor(L46-49)——纯 reducer-over-context:
const actor = createTransitionActor(initialContext, (context, message) => {
// reducer:返回新 context;副作用可同步执行(先于 signal 更新,L37-38 文档)
return { ...context, count: context.count + 1 };
});
它没有领域状态——value 只有 'active' | 'destroyed'(L14-15 文档:通用生命周期标记),全部有意思的状态都在 context 里。
send 的实现(L63-68)里有个细节:if (newContext !== context) setContext(newContext)——引用同一性检查既是 no-op 语义也是优化(L34-35:reducer 返回原 context 就跳过 signal 写入,不触发失效)。
什么时候用 transition actor?当「状态」其实就是数据累积(没有模式切换)时——比完整状态机轻得多。39 篇会看到 SegmentLoaderActor 用完整形态(它有真正的状态模式:规划/加载/等待)。
小结
- 快照即信号——
createMachineCore三行核心(信号 + untrack getState + update transition)。 - 状态表两级查表——
states[state].on[message.type],Extract 自动收窄消息类型。 - context 双读语义——dispatch 快照(
context)vs live 读(getContext()),串行 task 链的关键。 - post-destroy 双保险——显式检查 + 结构性查不到。
- onSettled 三重防陈旧——注册时机 + 回调验状态 + Runner 引用丢弃。
- transition/setContext 互补——一个换 value 保 context,一个换 context 保 value。
下一篇看 Reactor——信号驱动的状态机,Actor 的镜像兄弟。



被折叠的 条评论
为什么被折叠?



