fiber 与 effect:可逆副作用

专栏:Cordis 插件框架 · 第 8 / 12 篇
Cordisfibereffect

:::info 学习目标 完成本篇后你能够:使用 effect 的四种返回形态;解释 disposables 的收集与逆序清理;说出 epoch 机制防的是什么问题。 前置:第 5~7 篇完成。预计时长:60 分钟。 :::

“插件注册的一切都要可逆”是 Cordis 的核心承诺,实现载体就是 effectctx.effect(execute) 立即执行你的初始化函数,收集它产出的清理函数,在 fiber 卸载时按注册逆序执行。dsh 的每个 ctx.on、每个 provide、每次工具注册,底层都是一次 effect。

effect 接受的四种返回形态

// vendor/cordis/src/fiber.ts:83-93
export type Effect<T = any> =
  | SyncEffect<T>      // 同步:直接返回 disposer,或同步迭代器逐个 yield
  | AsyncEffect<T>     // 异步:Promise<disposer>,或异步迭代器逐个 yield
形态写法场景
同步函数ctx.effect(() => { setup(); return cleanup })最常见
同步迭代器ctx.effect(function* () { yield d1; yield d2 })多步注册(dsh 的 presentAs() 用它)
Promisectx.effect(() => fetchThenCleanup())异步初始化
异步迭代器生成器 + await多步异步注册

_execute(fiber.ts

附近)对四种形态分别处理,同步迭代器逐个 yield 逐个收集;异步迭代器还带 epoch 检查——每次 iter.next() 前比对 runner.epoch !== oldEpoch,fiber 已进入新一轮时立即停止收集。

effect 的收集与清理

图表(cordis-fiber-effect.md)

收集与清理:disposables 列表

收集与清理:disposables 列表

effect 执行期间产出的每个 disposer 都被收集:

// vendor/cordis/src/fiber.ts:447-455(节选)
const runner: EffectRunner<boolean> = {
  execute,
  epoch: true,
  collect: (dispose) => {
    disposables.push(dispose)
    this._disposables.delete(dispose)     // 从 fiber 的待清理清单摘出
    if (dispose[symbols.effect]) {
      meta.children.push(dispose[symbols.effect])   // 效果树:嵌套 effect 记为子节点
    }
  },
  getOuterStack: buildOuterStack(),
}

两个机制:收集时从 fiber 的全局待清理清单摘出(effect 自己管理自己那部分,卸载时不重复清理);EffectMeta 树记录 effect 的嵌套结构(getEffects() 可打印完整的”谁注册了什么”诊断树)。

epoch:异步产出的保险丝

_execute 开头捕获 oldEpoch = runner.epoch,异步迭代器的每个 next() 前都检查。epoch 在每次 _reload/_unload 时变化——热重载的瞬间,旧轮次还在慢慢初始化的异步 effect 会被静默丢弃,不会把副作用挂进新轮次。这就是为什么 dsh 能放心地热重载插件。

与其他模块的联系

  • registry(第 2 篇)ctx.plugin() 把新 fiber 的 dispose 注册为父 fiber 的 effect——父卸载级联卸载子(fiber.ts 构造器节选);
  • events(第 6 篇)EventsService.register 把监听器包成 effect 挂 fiber——监听器随插件卸载自动退订;
  • dshctx.tools.register() 返回的 disposer、ctx.on 的退订、定时器的 clearInterval,全部汇入同一个 effect 栈,getEffects() 可打印整棵诊断树。

常见踩坑

  1. 在 UNLOADING 状态创建 effect——直接抛 CordisError('INACTIVE_EFFECT'):清理期注册的副作用会逃过本轮卸载快照;
  2. 异步 effect 忘了返回 disposer 的 Promise——ctx.effect(async () => {...}) 不返回清理函数也合法(无清理),但要清理就 return async () => {...}
  3. 在 disposer 里抛异常——错误被 ctx.logger.error 记录,不影响其他 disposer,但你的清理确实没做完。

随堂练习(带验收标准)

  1. 写一个插件用同步迭代器 effect 注册三个定时器,dispose 后验证全部停止;
  2. 用异步迭代器 effect(每 100ms yield 一个)+ 立即 dispose。验收:异步产出被安全丢弃、进程正常退出;
  3. 调用 host.getEffects()(或读 fiber.ts 的 EffectMeta),打印你自己 fiber 的效果树。

← 返回文章列表