☰
videojs v10 源代码系列解读:33 · Behavior:通用组合单元
2026/10/1 6:35:26 网站建设 项目流程

03 篇预告过:「Behavior 是 SPF 区别于所有其他流媒体库的核心创新」。这篇正式拆它。Behavior 把信号、Actor、Reactor、Runner 全部统一成一种可组合单元——引擎就是一个 Behavior 列表。还会讲defineBehavior的穷尽性强制和makeShareSignals的消费者边界。下一篇讲 createComposition 的类型冲突检测。

回顾:Behavior 接口

create-composition.ts:83-93(03 篇引用过):

exportinterfaceBehavior<StateMap,ContextMap,Cfg>{stateKeys:readonly(keyofStateMap)[];// L89 — 声明需要的状态键contextKeys:readonly(keyofContextMap)[];// L91 — 声明需要的上下文键setup:(deps:BehaviorDeps)=>BehaviorCleanup;// L92}

一个 behavior声明自己需要哪些键(stateKeys/contextKeys),提供一个 setup 函数,返回可选清理。BehaviorDeps(L60-64)给 setup 传{ state, context, config }——槽位映射 + 静态配置。

BehaviorCleanup(L9):void | (() => void | Promise<void>) | { destroy(): void | Promise<void> }——三种形态,函数或带 destroy 的对象,可异步。

两种 flavor:简单 vs 原语增强

设计文档(27 篇引过)的核心论断:问题的核心从来不是「用 Behavior 还是 Actor?」,而是「用 Behavior 包裹什么?」

  • 简单 behavior——只用信号。stateKeys 声明槽位,setup 里computed/effect建响应关系。
  • 原语增强 behavior——内部实例化 actor/reactor/runner。actor/reactor/runner 永远活在 behavior 内部,从不出现在组合层。

这个规则我读源码验证过:setup-mediasource.ts、load-segments.ts这些 behavior 内部 new 了一堆 actor/reactor,但对外的形态仍然是标准的{ stateKeys, contextKeys, setup }。组合层只认 Behavior 一种东西——这就是「通用组合单元」的含义。

对比 hls.js:它有 StreamController、BufferController、LevelController……一堆 controller 类互相引用、互相调用方法。SPF 里这些全部变成 behavior,互相之间零直接引用——只通过信号槽位通信。

defineBehavior:穷尽性强制(L428-455)

手写 Behavior 字面量有个风险:类型声明了 state map,但 stateKeys 忘了列全某个键。运行时那个键的信号不会被创建,setup 里读它拿到 undefined,出了 bug 还很难查。

defineBehavior在编译期堵住这个漏洞。核心是ExhaustiveKeys(L375-379):

typeExhaustiveKeys<KeysextendsreadonlyPropertyKey[],Slotextendsobject,Nameextendsstring>=[keyofSlot]extends[Keys[number]]?Empty:{[Kin`Error:${Name}Keys must list every key in the typed slice`]:Exclude<keyofSlot,Keys[number]>};

判定keyof Slot ⊆ Keys[number](槽位类型的每个键都在 keys 数组里)。不满足时给参数 shape 加一个 phantom 字段——错误消息是模板字面量类型(Error: stateKeys must list every key in the typed slice),值是缺失键的列表。用户提供的值不可能满足这个字段,调用点直接编译报错,错误消息里列出漏了哪些键。

这是「把运行时 bug 提前到编译期」的又一手(和 store 的UnionSliceState、capability 的谓词一个思路,但更极致——错误消息本身是类型)。

还有个贴心细节(L432-433):defineBehavior的const修饰符让stateKeys: ['preload']直接推断为readonly ['preload'],调用点免写as const。

运行时呢?恒等(L450-454 只有as unknown as桥接)——defineBehavior 是纯类型检查器,零运行时成本。

makeShareSignals:消费者边界(53 行)

Behavior 们互相通过信号槽位通信,那外部消费者(比如引擎的使用者)怎么拿到这些信号?share-signals.ts的makeShareSignals(L41-44):

exportfunctionmakeShareSignals<S,C>(inputStateKeys=[],inputContextKeys=[]){return{stateKeys:inputStateKeys,// 透传(默认 [])contextKeys:inputContextKeys,setup:({state,context,config})=>{config.onSignalsReady?.({state,context});// L47 — setup 时刻交出完整映射},};}

它是个特殊 behavior:setup 不干活,只把 composition 的完整信号映射交给onSignalsReady回调。引擎使用者通过这个回调捕获信号引用,之后读写。

两个精妙点:

  1. 「capture refs, use later」是安全模式(L10-14 注释警告):回调触发时其他 behavior 还在 setup 阶段,读到的是初始种子值而非后续写入的值。所以正确用法是捕获引用(signal 的身份是稳定的),延迟使用——不要在回调里立刻.get()拿业务数据。
  2. inputStateKeys用来「物化」消费者输入槽(L29-39 注释):如果某个键没有任何 behavior 生产(比如引擎使用者要传入的userAudioTrackSelection),makeShareSignals 声明它,composition 就会为它创建信号。消费者输入也走同一套槽位机制。

还有一个类型层面的细节(L37-39):它刻意用Behavior<>字面量而非 defineBehavior——避免空/部分 keys 数组触发穷尽性检查。因为它的 setup 参数描述的是「回调收到的完整 S/C」,而非它物化的子集,穷尽性检查对它不适用。知道什么时候不用工具,和知道怎么用一样重要。

41 篇会看到 HLS 引擎用makeShareSignals作为对外的 I/O 边界。

Behavior 的读写权限模型

把 03 篇的伏笔正式收掉。behavior 的 setup 参数类型是槽位映射:

setup:(deps:{state:{presentation:ReadonlySignal<Presentation>,preload:Signal<Preload>},// ...})=>Cleanup

每个槽位自己决定Signal<T>(可写)或ReadonlySignal<T>(只读)——28 篇讲过ReadonlySignal是Omit<State<T>, 'set'>。TS 编译期拒绝在只读槽上调.set()。

读写意图是每个 behavior 自己声明的类型契约。composition 不强制谁拥有哪个键——多个 behavior 可以声明同一个键(一个写、其他读),类型冲突检测(下篇)保证它们对键的类型一致。

引擎 = Behavior 列表(预览)

41 篇详讲,这里先看形态。createSimpleHlsEngine大致是:

createComposition([resolvePresentation,// 简单 behavior:fetch + 解析清单 → presentation 槽位selectTracks,// 选择轨道 → selection 槽位setupMediaSource,// 原语增强:内部建 MediaSource → mediaSource 槽位setupBufferActors,// 原语增强:SourceBufferActor(video/audio)loadSegments,// 原语增强:SegmentLoaderActor + Reactor 翻译endOfStream,// ...abr,// ABR(5.75 步)makeShareSignals(['userAudioTrackSelection'],[],{onSignalsReady}),// 消费者边界],{config,initialState,initialContext});

约 20 个 behavior 按固定顺序。初始化顺序是承重的——比如syncPreload必须在resolvePresentation之前注册,两个 SourceBuffer 要在一个同步过程创建(Firefox 的mozHasAudio)。这个顺序约束是显式写在引擎代码里的知识,41 篇展开。

小结

  • Behavior 是唯一组合单元——actor/reactor/runner 活在 behavior 内部,组合层只认 Behavior。
  • behavior 之间零直接引用——只通过信号槽位通信(对比 hls.js 的 controller 互调)。
  • defineBehavior 穷尽性强制——漏列键编译期报错,错误消息列出缺失键;运行时恒等。
  • const 修饰符免 as const。
  • makeShareSignals 是消费者边界——onSignalsReady 交出信号映射;「capture refs, use later」;可物化消费者输入槽。
  • 读写权限是 setup 参数的类型契约——每槽位自选 Signal/ReadonlySignal。

下一篇拆createComposition的完整实现——运行时怎么构建信号映射、编译期怎么做类型冲突检测。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询