fiber 与 effect:可逆副作用
专栏:Cordis 插件框架 · 第 8 / 12 篇:::info 学习目标 完成本篇后你能够:使用 effect 的四种返回形态;解释 disposables 的收集与逆序清理;说出 epoch 机制防的是什么问题。 前置:第 5~7 篇完成。预计时长:60 分钟。 :::
“插件注册的一切都要可逆”是 Cordis 的核心承诺,实现载体就是 effect。ctx.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() 用它) |
| Promise | ctx.effect(() => fetchThenCleanup()) | 异步初始化 |
| 异步迭代器 | 生成器 + await | 多步异步注册 |
_execute(fiber.ts
iter.next() 前比对 runner.epoch !== oldEpoch,fiber 已进入新一轮时立即停止收集。
effect 的收集与清理
收集与清理: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——监听器随插件卸载自动退订; - dsh:
ctx.tools.register()返回的 disposer、ctx.on的退订、定时器的 clearInterval,全部汇入同一个 effect 栈,getEffects()可打印整棵诊断树。
常见踩坑
- 在 UNLOADING 状态创建 effect——直接抛
CordisError('INACTIVE_EFFECT'):清理期注册的副作用会逃过本轮卸载快照; - 异步 effect 忘了返回 disposer 的 Promise——
ctx.effect(async () => {...})不返回清理函数也合法(无清理),但要清理就return async () => {...}; - 在 disposer 里抛异常——错误被
ctx.logger.error记录,不影响其他 disposer,但你的清理确实没做完。
随堂练习(带验收标准)
- 写一个插件用同步迭代器 effect 注册三个定时器,dispose 后验证全部停止;
- 用异步迭代器 effect(每 100ms yield 一个)+ 立即 dispose。验收:异步产出被安全丢弃、进程正常退出;
- 调用
host.getEffects()(或读 fiber.ts 的 EffectMeta),打印你自己 fiber 的效果树。