news 2026/10/1 14:20:15

videojs v10 源代码系列解读:32 · Reactor 模型:信号驱动的状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
videojs v10 源代码系列解读:32 · Reactor 模型:信号驱动的状态机

Reactor 是 Actor 的镜像兄弟:由信号驱动而非消息驱动。它的核心是三个概念——monitor(派生状态自动转换)、entry(进状态跑一次)、effects(依赖变化重跑)。实现只有 192 行,但把 28 篇的信号机制用到了极致:所有生命周期都搭载在 effect 的 cleanup 契约上。读完这篇你会理解「薄翻译器」到底薄在哪。

什么时候用 Reactor 而不是 Actor

回忆 27 篇的决策规则:由消息驱动 → Actor;由信号驱动 → Reactor。

具体判断(来自 conventions 文档):一个单元需要 Actor,当它拥有资源+串行工作+离散事件输入(SourceBuffer:MSE 资源,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: Record<State, ...>不是 Partial——每个合法状态都必须声明(空效果传{})。这是刻意为之:漏声明一个状态就漏掉它的行为,类型系统强制完整性。(对比 Actor 的Partial<Record<...>>——Actor 允许「这个状态不处理任何消息」,Reactor 不允许「这个状态什么效果都没有」不声明。其实语义都允许空,但 Reactor 要求显式写出来。)

实现骨架:descriptors + toEffect(L133-173)

Reactor 的实现思路很统一:把 monitor/entry/effects 全部编译成「descriptor 数组」,再用 28 篇的effect()统一执行。

descriptor 结构(L133-137)

typeEffectDescriptor={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:()=>{consttarget=fn();// tracked 求值 deriveFnif(target!==(getState()asState))transition(target);// 不等才转},shouldSkip:isTerminal,// 终态下 monitor 停转})),// 2. per-state descriptors(L156-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 flush

monitor 在前,per-state effects 在后——同一个 flush 里,monitor 触发的 transition 先生效,per-state effects 再按新状态重求值。如果顺序反了,effects 会用旧状态跑一轮再被新状态重跑,浪费且可能出 bug。

toEffect:统一的执行包装(L165-171)

consttoEffect=({fn,shouldSkip,toFnCall=(baseCall)=>baseCall})=>effect(()=>{constsnapshot=snapshotSignal.get();// L167 — tracked 读!if(shouldSkip(snapshot))return;// 门控不通过,跳过constbaseCall=()=>fn();returnwrapResult(toFnCall(baseCall)());// cleanup 归一化});

L167 是整个 Reactor 的枢纽:snapshotSignal.get()是tracked 读(在 effect 的 computed 内),因此每次 transition 都重触发该 effect;shouldSkip门控决定这个 descriptor 归不归当前状态管。

「进入状态 X 时跑 entry」的机制 = transition 改变 snapshot signal → 所有 descriptor 的 effect 失效重跑 →shouldSkip(isNotState)对 X 的 descriptor 放行、其他跳过 → X 的 entry/effects 执行。「退出状态时清理」= 同一机制:effect 重跑前先执行上次的 cleanup(28 篇 effect 的 L34)。

状态生命周期完全搭载在 effect 的 cleanup 契约上——Reactor 自己没有写一行「退出状态时怎么清理」的代码。这是「薄」的极致。

wrapResult:cleanup 归一化(L125-129)

constwrapResult=(result)=>{// 函数 → 原样;{abort()} 对象 → 包装成 () => result.abort();falsy → undefined};

effect 返回fn | {abort()} | void三种形态,归一化成() => void交给 effect 机制。{abort()}形态很实用——直接把 Task 的 controller 或 DOM 句柄交出去。

两步销毁(L180-189)

destroy(){if(isTerminal())return;// 幂等transition('destroying');// L186 — 第一步transition('destroyed');// L187 — 第二步(同步紧跟)for(constdisposeofeffectDisposals)dispose();// L188 — dispose 所有 effect}

L183-185 注释解释了为什么两步:先'destroying'为将来异步 teardown 预留,随后立即'destroyed'覆盖当前的同步场景。目前两步是同步连转——但类型系统里'destroying'的存在让未来加异步清理不用改 API。

对比 Actor 的单步 destroy(31 篇)——Reactor 有 effect 需要逐个 dispose(L188),dispose内部是watcher.unwatch(c)+ 执行最后一次 cleanup(28 篇 effect.ts L39-42)。

一个完整例子:加载分段反应器

用 41 篇会遇到的场景写个概念示例:

createMachineReactor({initial:'idle',monitor:[()=>{constmediaSource=state.mediaSource.get();// tracked 读constsource=state.source.get();// tracked 读if(!mediaSource||!source)return'idle';return'active';},],states:{idle:{},active:{effects:[()=>{// 订问当前时间,决定加载窗口constmedia=context.media.get();constunlisten=media.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)信号(依赖变化)
状态表Partial<Record>Record(必须全声明)
per-state 钩子on(消息→handler)、onSettledentry(一次)、effects(重跑)
context有(双读语义)无(纯 value)
runner可选集成无
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 重触发 effect,shouldSkip 门控。
  • cleanup 全搭载 effect 契约——退出状态的清理零手写,「薄」的极致。
  • 两步销毁——为异步 teardown 预留。

下一篇是组合层——Behavior,把信号、Actor、Reactor 全部统一成一种可组合单元。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 14:18:17

聊城通风天窗源头厂家推荐:售后完善定制方案

很多人在找聊城通风天窗厂家的时候&#xff0c;都会踩过不少坑。要么选到的产品通风量不够用&#xff0c;车间依旧闷热憋闷;要么产品防水抗风性能差&#xff0c;下几场雨就出现渗漏&#xff0c;台风天还担心被吹坏;还有的厂家只管卖货不管安装&#xff0c;自己装完要么漏烟要么…

作者头像 李华
网站建设 2026/10/1 14:18:00

从零搭建AI工程:提示词、Agent与RAG实战指南

如果你在 GitHub 上搜过 ai-engineering-from-scratch 这个名字&#xff0c;应该能猜到它不是一个“调几个 API 跑个 demo”的玩具项目。它是一个从零到一、把大模型应用从想法推到生产环境的完整方法论&#xff0c;核心覆盖提示词工程&#xff08;Prompt Engineering&#x…

作者头像 李华
网站建设 2026/10/1 14:17:59

聚合AI外贸GEO服务商哪家强?适用于小语种站点与Google/Bing双引擎优化

海外买家正在转向AI提问&#xff0c;外贸获客逻辑已经变了全球贸易数字化进程正在加速&#xff0c;海外采购商的决策入口正在发生根本性迁移。过去是搜关键词、打开网页、逐一比较&#xff0c;如今越来越多买家直接向ChatGPT、Gemini、Claude等AI提问&#xff1a;有哪些靠谱的中…

作者头像 李华
网站建设 2026/10/1 14:17:21

英辰朗迪AI获客每日AI精选(2026.09.30)

一、技术前沿第1条&#xff1a;Anthropic 发布 Claude Sonnet 5.5&#xff0c;速度提升 30%、成本下降 30%核心内容&#xff1a;Anthropic 于 9 月 29 日推出 Claude Sonnet 5.5&#xff0c;为 Claude 5.5 家族第二款模型。相比 Sonnet 5&#xff0c;API 响应速度提升超过 30%&…

作者头像 李华
网站建设 2026/10/1 14:17:17

ARP欺骗攻击防御全攻略:DHCP Snooping与DAI实战详解

先说一个比较扎心的现实&#xff1a;很多网络工程师对ARP的理解停留在“IP查MAC”这五个字上&#xff0c;但真到了排障现场&#xff0c;遇到全网掉线、网关MAC被篡改、内网抓包全是大量重复ARP广播的时候&#xff0c;才发现对ARP的认知远远不够。这篇是我个人实验笔记的第三期&…

作者头像 李华
网站建设 2026/10/1 14:17:01

单视频流三维实时重构在海关货物查验与异常行为智能识别中的应用方案

一、项目概述当前海关货物查验与现场作业监管普遍存在二维视频监管维度单一、货物堆位空间关系识别困难、人员异常行为漏判、查验过程不可量化、风险预警被动滞后、现场态势与监管数据脱节等行业痛点。传统海关查验监管主要依赖人工视频巡查、纸质记录、事后回看与经验判断&…

作者头像 李华