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:02
heartbeat 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)

动手练习

  1. 把 setTimeout 的 700ms 改成 300ms,观察 tick 次数变化——effect 主体与 disposer 的时序关系不变
  2. 给 heartbeat 再加一个 ctx.effect()(比如另一个定时器),卸载时观察两个 disposer 按注册逆序执行
  3. 把 disposer 改成 async(内部 await new Promise(r => setTimeout(r, 100))),确认 fiber.dispose() 会等它完成才打印 disposed

上一课:01-first-plugin | 下一课:03-services