
Reactor 是 Actor 的镜像兄弟由信号驱动而非消息驱动。它的核心是三个概念——monitor派生状态自动转换、entry进状态跑一次、effects依赖变化重跑。实现只有 192 行但把 28 篇的信号机制用到了极致所有生命周期都搭载在 effect 的 cleanup 契约上。读完这篇你会理解「薄翻译器」到底薄在哪。什么时候用 Reactor 而不是 Actor回忆 27 篇的决策规则由消息驱动 → Actor由信号驱动 → Reactor。具体判断来自 conventions 文档一个单元需要 Actor当它拥有资源串行工作离散事件输入SourceBufferMSE 资源append/remove 必须串行输入是离散命令。一个单元适合 Reactor当它观察状态变化并做出反应——「源变成了 X我该发消息告诉 Y」。设计文档对 Reactor 的定位是薄翻译器只回答「要不要发消息、发什么消息」不含业务逻辑。业务逻辑在 behavior 的其他部分或纯函数里。ReactorDefinition声明一台反应器create-machine-reactor.ts的定义类型constdef{initial:idle,// L54monitor:[// L60 — 单函数或数组()selectStateFromSignals(),// deriveFn体内读信号自动成为依赖],states:{// L66 — Record不是 Partial每个状态必须声明idle:{entry:[...],effects:[...]},loading:{effects:[...]},// 空效果传 {}},};三个部件的语义L33-35 文档monitor——deriveFn 在 tracked effect 中求值依赖变化重新求值返回值 ≠ 当前状态时自动 transition。entry——进状态时跑一次函数体自动 untrack适合一次性 setup。effects——进状态时跑体内 tracked 信号变化时重跑要排除追踪需手写untrack()。注意 L66 的states: RecordState, ...不是 Partial——每个合法状态都必须声明空效果传{}。这是刻意为之漏声明一个状态就漏掉它的行为类型系统强制完整性。对比 Actor 的PartialRecord...——Actor 允许「这个状态不处理任何消息」Reactor 不允许「这个状态什么效果都没有」不声明。其实语义都允许空但 Reactor 要求显式写出来。实现骨架descriptors toEffectL133-173Reactor 的实现思路很统一把 monitor/entry/effects 全部编译成「descriptor 数组」再用 28 篇的effect()统一执行。descriptor 结构L133-137typeEffectDescriptor{fn:()void;shouldSkip:(snapshot:{value:string})boolean;// 门控这个状态下要不要跑toFnCall?:(baseCall)EffectCall;// 包装entry 用它加 untrack};descriptors 数组的构建L148-163——顺序是保证constdescriptors[// 1. monitor descriptors 排最前L149-155...toArray(def.monitor).map((fn)({fn:(){consttargetfn();// tracked 求值 deriveFnif(target!(getState()asState))transition(target);// 不等才转},shouldSkip:isTerminal,// 终态下 monitor 停转})),// 2. per-state descriptorsL156-162...Object.entries(def.states).flatMap(([state,stateDef]){constisNotState(snapshot)snapshot.value!state;// L157return[{fn:entry,shouldSkip:isNotState,toFnCall:untracked},// L159 — entry 加 untrack{fn:effect,shouldSkip:isNotState},// L160 — effects 不加];}),];L144-147 的注释明确了这个顺序保证monitor descriptors are built first — the ordering guarantee ensures transitions they trigger take effect before per-state effects re-evaluate in the same flushmonitor 在前per-state effects 在后——同一个 flush 里monitor 触发的 transition 先生效per-state effects 再按新状态重求值。如果顺序反了effects 会用旧状态跑一轮再被新状态重跑浪费且可能出 bug。toEffect统一的执行包装L165-171consttoEffect({fn,shouldSkip,toFnCall(baseCall)baseCall})effect((){constsnapshotsnapshotSignal.get();// L167 — tracked 读if(shouldSkip(snapshot))return;// 门控不通过跳过constbaseCall()fn();returnwrapResult(toFnCall(baseCall)());// cleanup 归一化});L167 是整个 Reactor 的枢纽snapshotSignal.get()是tracked 读在 effect 的 computed 内因此每次 transition 都重触发该 effectshouldSkip门控决定这个 descriptor 归不归当前状态管。「进入状态 X 时跑 entry」的机制 transition 改变 snapshot signal → 所有 descriptor 的 effect 失效重跑 →shouldSkipisNotState对 X 的 descriptor 放行、其他跳过 → X 的 entry/effects 执行。「退出状态时清理」 同一机制effect 重跑前先执行上次的 cleanup28 篇 effect 的 L34。状态生命周期完全搭载在 effect 的 cleanup 契约上——Reactor 自己没有写一行「退出状态时怎么清理」的代码。这是「薄」的极致。wrapResultcleanup 归一化L125-129constwrapResult(result){// 函数 → 原样{abort()} 对象 → 包装成 () result.abort()falsy → undefined};effect 返回fn | {abort()} | void三种形态归一化成() void交给 effect 机制。{abort()}形态很实用——直接把 Task 的 controller 或 DOM 句柄交出去。两步销毁L180-189destroy(){if(isTerminal())return;// 幂等transition(destroying);// L186 — 第一步transition(destroyed);// L187 — 第二步同步紧跟for(constdisposeofeffectDisposals)dispose();// L188 — dispose 所有 effect}L183-185 注释解释了为什么两步先destroying为将来异步 teardown 预留随后立即destroyed覆盖当前的同步场景。目前两步是同步连转——但类型系统里destroying的存在让未来加异步清理不用改 API。对比 Actor 的单步 destroy31 篇——Reactor 有 effect 需要逐个 disposeL188dispose内部是watcher.unwatch(c) 执行最后一次 cleanup28 篇 effect.ts L39-42。一个完整例子加载分段反应器用 41 篇会遇到的场景写个概念示例createMachineReactor({initial:idle,monitor:[(){constmediaSourcestate.mediaSource.get();// tracked 读constsourcestate.source.get();// tracked 读if(!mediaSource||!source)returnidle;returnactive;},],states:{idle:{},active:{effects:[(){// 订问当前时间决定加载窗口constmediacontext.media.get();constunlistenmedia.addEventListener(timeupdate,(){loader.send({type:checkBuffer});});returnunlisten;// cleanup退出 active 或依赖变化时自动执行},],},},});mediaSource 或 source 变 undefined → monitor 返回 ‘idle’ ≠ ‘active’ → 自动 transition → active 的 effect cleanup 自动跑解绑 timeupdate→ idle。整个退出逻辑零手写。Actor 与 Reactor 对照表ActorReactor驱动消息send信号依赖变化状态表PartialRecordRecord必须全声明per-state 钩子on消息→handler、onSettledentry一次、effects重跑context有双读语义无纯 valuerunner可选集成无destroy单步destroyed两步destroying→destroyed典型用途SourceBufferActor、SegmentLoaderActorloadSegments 的翻译层、track 同步Reactor 无 context 这个差异值得注意——它真的只是「状态翻译器」数据都在外部的 composition 信号里33 篇。小结monitor 自动转换——deriveFn tracked 求值返回值 ≠ 当前状态即 transition。entry untracked / effects tracked——一次性 setup vs 依赖驱动重跑。descriptors 顺序是保证——monitor 先于 per-state effects同 flush 里 transition 先生效。L167 的 tracked snapshot 读是枢纽——transition 重触发 effectshouldSkip 门控。cleanup 全搭载 effect 契约——退出状态的清理零手写「薄」的极致。两步销毁——为异步 teardown 预留。下一篇是组合层——Behavior把信号、Actor、Reactor 全部统一成一种可组合单元。