依赖注入:inject 与门控
专栏:Cordis 插件框架 · 第 5 / 12 篇:::info 学习目标
完成本篇后你能够:解释依赖门控的完整机制(PENDING→唤醒→级联加载);读懂 Inject.resolve 的三种声明形式;亲手验证”服务消失会级联卸载依赖方”。
前置:第 4 篇完成。预计时长:50 分钟。
:::
插件的 inject = ['tools'] 只有一行,背后却是整个框架最精巧的机制:依赖就绪前插件保持 PENDING;依赖消失时插件级联卸载;依赖恢复时自动重新加载。本篇从 fiber.ts 的源码把它讲透。
三种声明形式
Inject 类型(registry.ts
Inject.resolve(registry.ts)把它们归一成 服务名 → 拦截配置 的映射:
// registry.ts:19
export type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
// 三种等价的声明
inject = ['tools'] // 数组:只要服务
inject = { tools: null } // 对象:无拦截配置
inject = { tools: { mode: 'ptc' } } // 对象:带 intercept 配置
// 类还支持 @Inject 装饰器逐个声明(registry.ts:37-60,TC39 stage-3 装饰器)
对象形式的值不是随便填的——它是该插件上下文里这个服务的 intercept 配置(第 4 篇的合并顺序在这里生效:fiber 构造器把非空 intercept 写进子 ctx 的 intercept 映射,fiber.ts 构造器节选)。
门控的实现:epoch 与 refresh
fiber 创建时把 inject 解析结果存下来,然后对每个依赖名执行 _checkImpl(name)(fiber.ts
_refresh(fiber.ts)是门控的决策核心:
// vendor/cordis/src/fiber.ts:611-645(节选,结构还原)
_refresh() {
// 依赖快照:全部就绪 → 存入 fiber.store;任一缺失 → epoch=INACTIVE
...
this._updateState(() => {
if (this._epoch !== INACTIVE) {
this.inertia = this._reload() // 依赖齐了 → LOADING → 执行插件
return FiberState.LOADING
} else if (this.state === FiberState.LOADING || this.state === FiberState.ACTIVE) {
this.inertia = this._unload() // 依赖没了 → UNLOADING
return FiberState.UNLOADING
}
})
}
而唤醒的另一半在 reflect 层:provide 与注销都会调 notify([name])(reflect.ts
_refresh。于是:
这套”依赖变化 → 通知 → 重算 → 状态迁移”的机制,让热重载、服务替换、级联卸载全部变成框架的自然行为而非特判。
epoch:防过期效果的保险
_refresh 每次都会推进 fiber 的 epoch(一个递增令牌)。_execute 执行 effect 时捕获当时的 epoch,异步 effect(如 async 生成器)每次产出新 disposer 前都检查 runner.epoch !== oldEpoch——fiber 已经开始新一轮加载/卸载时,旧轮次的异步产出直接丢弃(fiber.ts
if (runner.epoch !== oldEpoch) return)。没有这个保险,热重载时旧插件的异步初始化会把副作用注册到新轮次里。
与其他模块的联系
- registry(第 2 篇):
ctx.inject(deps, callback)就是plugin({ inject, apply: callback })的语法糖(registry.ts); - fiber(第 8~9 篇):epoch/state/store 都在 fiber 上——本篇的机制就是它的生命周期内核;
- dsh:
tool-fs的inject = ['tools', 'fs', 'systemPrompt']让它在三大服务全部就绪后才注册工具——顺序错了连 schema 都拿不到。
常见踩坑
- 在 PENDING 期读写 fiber 上的插件状态——apply 还没执行,状态不存在;依赖门控保证 apply 只在服务齐后运行,别抢跑;
- inject 写了服务名但类型没声明合并——运行时正常,只是没有类型提示;记得
declare module; - 以为 inject 是”一次性检查”——它是持续的订阅关系:服务消失你也会被卸载。需要”可选依赖”用
ctx.get()探测而非 inject。
随堂练习(带验收标准)
- 写提供方(延迟 2 秒后 provide)与消费方(inject),验证消费方的 apply 确实在 2 秒后才执行;
- 卸载提供方 fiber。验收:消费方被级联卸载(its cleanup 执行);重新 provide 后消费方自动重载;
- 在消费方 apply 里打印
ctx.fiber.state,分别在启动时与依赖消失时观察状态值变化。