ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

videojs v10 源代码系列解读:34 · `createComposition`:类型安全的冲突检测

videojs v10 源代码系列解读:34 · `createComposition`:类型安全的冲突检测 卷五核心篇的收官。createComposition把 behavior 列表组装成引擎运行时构建信号映射、跑 setup、收 cleanup编译期做跨 behavior 的类型冲突检测。那套ValidateComposition/HasConflict类型体操是全 v10 最精妙的 TS 代码——两个 behavior 对同一个键声明了冲突类型错误以字符串字面量类型出现在参数位置。这篇完整拆它。运行时部分三个步骤先看简单的运行时L306-358。步骤 1buildSignalMap——从键并集构建信号L295-304functionbuildSignalMapS(keys:IterablePropertyKey,initial:PartialS){constuniqueKeysnewSet(keys);// L300 — 去重首现优先returnObject.fromEntries([...uniqueKeys].map((key)[key,signal(init[key])])// L301 — 每键一个信号)as{[KinkeyofS]-?:SignalS[K]};}输入所有 behavior 的stateKeys并集new Set去重保持插入序每个唯一键建一个signal()从initialState[key]播种缺省即undefined。L278-281 注释坦白per-key 类型只在 TS 层runtime 全是Signalunknown。步骤 2跑 setup 收 cleanupL323-337conststatebuildSignalMap(validBehaviors.flatMap((b)b.stateKeys),options?.initialState??{});// L323-326constcontextbuildSignalMap(validBehaviors.flatMap((b)b.contextKeys),options?.initialContext??{});// L327-330constdeps{state,context,config:options?.config??{}};// L332-336constcleanupsvalidBehaviors.map((behavior)behavior.setup(deps));// L337所有 behavior 共享同一份 deps同一套信号映射。setup 按数组顺序同步执行返回值收进 cleanups。步骤 3destroy——异步清理 信号重置L342-358asyncdestroy(){constresults[];for(constcleanupofcleanups){if(cleanupnull)continue;// L345if(typeofcleanupfunction)results.push(cleanup());// L346-347elseif(destroyincleanup)results.push(cleanup.destroy());// L348-349}awaitPromise.all(results);// L352 — 等全部清理完成for(constsigofObject.values(state))sig.set(undefined);// L356 — 重置所有信号for(constsigofObject.values(context))sig.set(undefined);}两类 cleanup函数 / destroy 对象分类收集await Promise.all等所有清理完成异步清理如「等最后一个 append 完成」最后把每个信号重置为undefinedL356-357。重置的意义销毁后任何残留的 effect/读侧立刻看到「空」不会读到陈旧数据。L353-355 注释说这对应旧版owners.set({})的语义。编译期部分冲突检测的完整链路现在到重头戏。目标是behavior A 声明preload: Signalauto|metadatabehavior B 声明preload: Signalnumber——组合时编译报错。第一环InferBehaviorState——从 setup 签名推导L107-136typeDepsOfBBextends{setup:(deps:inferD,...args:any[])any}?D:never;// L107typeUnwrapSignalsMMextendsobject?{[KinkeyofM]:M[K]extends{get():inferV}?V:never}:Empty;// L127typeInferBehaviorStateFUnwrapSignalsDepsOfF[state];// L130从 behavior 的setup 函数签名抽出 deps 类型再从state字段解包每个槽位的值类型。L120-126 注释解释了一个关键选择经由{ get(): infer V }而非Signalinfer V推断——绕开 Signal 类型的 nominal/不变性得到协变的 V。第二环IntersectBehaviors——递归交集L146-151typeIntersectBehaviorsBehaviorsextendsreadonlyAnyBehavior[],ProjectBehaviorsextendsreadonly[inferFirst,...inferRestextendsreadonlyAnyBehavior[]]?ApplyProject,FirstIntersectBehaviorsRest,Project:Empty;对元组递归第一个的投影 剩下的递归结果空元组基例Empty。L139-145 注释解释为什么不用UnionToIntersection的逆变技巧——避免空{}成员导致塌缩。三个投影 markerL166-180分发到 InferBehaviorState/Context/Config最终得到ResolveBehaviorStateBehaviorsL175-176——所有 behavior 状态类型的交集。第三环HasConflict——检测塌缩L189-193typeHasConflictTextendsobjecttrueextends{[KinkeyofT]:[T[K]]extends[undefined]?true:never}[keyofT]?true:false;这是检测器本体。原理TS 的交叉类型遇到冲突会塌缩。必选冲突{v: number} {v: string}→{v: never}。[never] extends [undefined]是 → true。可选冲突{v?: number} {v?: string}→{v?: undefined}。直接命中 → true。L184-188 注释列出这两条捕获路径。交叉类型的塌缩行为从 bug 变成了特性——冲突检测不需要逐对比较直接看交集里有没有字段塌成 never/undefined。第四环ValidateComposition——闸门L211-218typeValidateCompositionBehaviorsHasConflictResolveBehaviorStateBehaviorsextendstrue?Error: behaviors have conflicting state types:HasConflictResolveBehaviorContextBehaviorsextendstrue?Error: behaviors have conflicting context types:HasConflictResolveBehaviorConfigBehaviorsextendstrue?Error: behaviors have conflicting config types:[...Behaviors];依次检查 state → context → config。冲突时返回错误字符串字面量类型全部通过返回原元组。闸门怎么生效L306-321exportfunctioncreateCompositionconstBehaviorsextendsreadonlyAnyBehavior[](behaviors:ValidateCompositionBehaviors,// L307 — 编译期闸门options?:CompositionOptions...):Composition...{constvalidBehaviorsbehaviorsasunknownasreadonlyAnyBehavior[];// L318-321 — 运行时桥接// ...}L307 是闸门的全部behaviors参数类型就是ValidateCompositionBehaviors。冲突时这个类型是Error: behaviors have conflicting state types——你的 behavior 数组不是字符串无法赋给它调用点直接编译报错。类型检查通过时函数体只在成功分支运行L318 的as unknown as桥接回运行时形态。错误即类型——不需要自定义错误类、不需要ts-expect-error错误消息就是参数类型本身。我第一次读懂这个手法时是真的拍案。一个演示// behavior A 的 setup 参数声明了 state.preload: SignalPreloadMode// behavior B 的 setup 参数声明了 state.preload: SignalnumbercreateComposition([behaviorA,behaviorB]);// 编译错误Argument of type [BehaviorA, BehaviorB] is not assignable to// parameter of type Error: behaviors have conflicting state types.错误消息直接出现在参数类型位置。修复方式让两个 behavior 对preload的类型声明一致——类型冲突逼你回到设计谁拥有这个键的写权、它的类型到底是什么。类型体操的两个防御性细节这套类型代码里有两个「防坑」注释值得单独说Empty {}而非objectL109-118object {x: T}在 union-to-intersection 转换下会塌缩成{x: never}TS 的怪癖{} {x: T}则干净地简化成{x: T}。biome-ignore 注释都写了。[T[K]] extends [undefined]的方括号包一层元组防止分配条件类型distributive conditional把检查拆散到联合成员上。类型体操不是炫技——每一步都在防御 TS 推理的边角行为。三个 composition 级别的清理语义对比至此 v10 有三层清理机制对比一下层清理机制时机storeAbortControllerRegistrybase/supersede/cleardetach/destroy 时 abortelementDestroyMixin 双 rAF hostDestroyed永久离开 DOM 后SPF compositionsetup 返回 cleanupdestroy 时 await Promise.all 信号重置显式 destroy()SPF 的特点清理可以是异步的L352 await——MSE 的操作必须等真正完成。而信号重置L356是其他两层没有的销毁后读侧立刻看到空。小结运行时三步键并集 → buildSignalMap → setup 收 cleanupdestroy 时 await 全部清理再重置信号。编译期四环DepsOf 推导 → UnwrapSignals经 get() 绕 nominal→ 递归交集 → HasConflict 检测塌缩。错误即类型冲突时参数类型变成错误字符串调用点报错。交叉塌缩从 bug 变特性{v:number} {v:string}→{v:never}就是冲突信号。类型体操处处防御Empty {}、[T[K]]方括号都是 TS 边角行为的解药。至此 SPF 的框架核心信号 → Task/Runner → Actor/Reactor → Behavior → Composition讲完了。接下来 7 篇进入流媒体领域HLS 解析器、ABR、缓冲数学、MSE 管线、两个核心 Actor、引擎编排。
返回列表