依赖注入:inject 与门控

专栏:Cordis 插件框架 · 第 5 / 12 篇
Cordis依赖注入

:::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

)——把所有等待这个服务的 fiber 找出来重新 _refresh。于是:

图表(cordis-dependency-injection.md)

这套”依赖变化 → 通知 → 重算 → 状态迁移”的机制,让热重载、服务替换、级联卸载全部变成框架的自然行为而非特判。

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 上——本篇的机制就是它的生命周期内核;
  • dshtool-fsinject = ['tools', 'fs', 'systemPrompt'] 让它在三大服务全部就绪后才注册工具——顺序错了连 schema 都拿不到。

常见踩坑

  1. 在 PENDING 期读写 fiber 上的插件状态——apply 还没执行,状态不存在;依赖门控保证 apply 只在服务齐后运行,别抢跑;
  2. inject 写了服务名但类型没声明合并——运行时正常,只是没有类型提示;记得 declare module
  3. 以为 inject 是”一次性检查”——它是持续的订阅关系:服务消失你也会被卸载。需要”可选依赖”用 ctx.get() 探测而非 inject。

随堂练习(带验收标准)

  1. 写提供方(延迟 2 秒后 provide)与消费方(inject),验证消费方的 apply 确实在 2 秒后才执行;
  2. 卸载提供方 fiber。验收:消费方被级联卸载(its cleanup 执行);重新 provide 后消费方自动重载;
  3. 在消费方 apply 里打印 ctx.fiber.state,分别在启动时与依赖消失时观察状态值变化。

← 返回文章列表