02 · 生命周期与可逆 Effect
目标:理解「注册是可逆副作用」——插件卸载时全部副作用自动回滚,这是 Cordis 时间可组合性的落地。 对应快速入门项目:
npm run lesson:02
核心机制
- 通过 Cordis API 建立的注册(
ctx.on()、ctx.plugin()、服务注册)都是 effect,随插件卸载自动撤销 - 框架管不到的资源(定时器/连接/文件 watcher)要自己包进
ctx.effect()并返回 disposer - disposer 按注册逆序执行;多个异步 disposer 之间并发——有顺序要求的清理放进同一个 disposer 内依次 await
fiber.dispose()等待全部清理(含异步)完成,并递归卸载子插件
实例:心跳插件(自动清理演示)
// heartbeat.ts
import type { Context } from 'cordis'
export const name = 'lifecycle-demo'
function heartbeat(ctx: Context) {
console.log('heartbeat plugin loading')
ctx.effect(() => {
// effect 主体在加载期间运行
const timer = setInterval(() => console.log('tick'), 200)
// 返回的 disposer 在卸载期间运行
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
export function apply(ctx: Context) {
// ctx.plugin() 把代码里的函数直接挂为子插件,返回 fiber(运行时句柄)
const fiber = ctx.plugin(heartbeat)
ctx.effect(() => {
const timer = setTimeout(async () => {
await fiber.dispose() // 等待全部清理完成
console.log('disposed')
process.exit(0)
}, 700)
return () => clearTimeout(timer)
})
}# cordis.yml
- name: './heartbeat.ts'运行与预期输出:
npm run lesson:02heartbeat plugin loading
tick
tick
tick
heartbeat cleaned up ← disposer 自动执行,无需手动清理
disposed
要点:
ctx.effect(execute)的执行结果归一为四种形态:函数/可舍弃值直接收集、Promise 用 then 收集、同步迭代器逐项收集、异步迭代器逐项 await(每轮检查 epoch 是否失效)——回收时统一转成一串 disposer(核心实现在packages/core/src/fiber.ts,486 行)- 生命周期与插件一致的资源绝不需要手动清理——这就是「可逆 effect」的工程纪律:任何注册都返回一个 disposer,卸载即逆序出栈
Fiber 状态机
每个已加载插件实例有一个 fiber:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
- PENDING:已声明,等待所需服务(合法状态,不是错误,下一课见)
- FAILED:apply 或配置校验抛异常
- 本例中
heartbeat子插件走完整圈:LOADING → ACTIVE(tick)→ UNLOADING → DISPOSED(cleaned up)
动手练习
- 把
setTimeout的 700ms 改成 300ms,观察 tick 次数变化——effect 主体与 disposer 的时序关系不变 - 给
heartbeat再加一个ctx.effect()(比如另一个定时器),卸载时观察两个 disposer 按注册逆序执行 - 把 disposer 改成
async(内部await new Promise(r => setTimeout(r, 100))),确认fiber.dispose()会等它完成才打印disposed
上一课:01-first-plugin | 下一课:03-services