videojs v10 源代码系列解读:31 · Actor 模型:消息驱动的有限状态机

原语阶梯第三级——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 };
}

三件事浓缩了整个机器模型:

  1. 快照就是一个信号(L43)——状态变化天然响应式,订阅者自动感知。
  2. getState 用 untrack 读(L44)——在 Computed/Effect 里查状态不建立依赖,避免意外订阅。
  3. 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 的镜像兄弟。

上一篇: videojs v10 源代码系列解读:30 · `SerialRunner` 与 `ConcurrentRunner`
下一篇: videojs v10 源代码系列解读:32 · Reactor 模型:信号驱动的状态机
逻辑玩家
博客等级 码龄19年 75粉丝 108原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值