Cordis 入门 02:用 effect 管好插件副作用
第 1 篇只跑了一个打印日志的插件。真实插件通常会做更多事情:注册监听器、启动定时器、打开连接、创建资源。这些动作有一个共同点:插件函数结束后,它们仍然会影响运行时。
这类动作就是副作用。Cordis 入门最重要的习惯是:副作用要交给 ctx.effect(),并返回清理函数。
这篇要练什么
本文继续使用 cordis-demo/index.mjs。练完以后,你会写出一个可以启动、运行、卸载并清理定时器的插件。
你会掌握:
- 什么是 effect。
- 为什么裸副作用容易泄漏。
- 如何返回 disposer 清理副作用。
- 如何通过
fiber.dispose()卸载插件。
先看一个错误写法
把 index.mjs 改成下面这样:
js
import { Context } from 'cordis'
const ctx = new Context()
function badClockPlugin(ctx) {
setInterval(() => {
console.log('tick')
}, 1000)
}
const fiber = ctx.plugin(badClockPlugin)
await fiber
setTimeout(async () => {
console.log('dispose plugin')
await fiber.dispose()
}, 3100)这个插件可以运行,但写法有隐患。setInterval() 创建的定时器没有交给 Cordis 管理。插件卸载时,Cordis 不知道这个定时器存在,也就无法自动清理。
运行时你会发现,打印 dispose plugin 后,进程可能仍然继续输出 tick。
这就是插件系统里最常见的泄漏:插件卸载了,插件留下的东西还活着。
正确写法:ctx.effect()
把代码改成下面这样:
js
import { Context } from 'cordis'
const ctx = new Context()
function clockPlugin(ctx) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('tick')
}, 1000)
console.log('timer started')
return () => {
clearInterval(timer)
console.log('timer stopped')
}
}, 'clock timer')
}
const fiber = ctx.plugin(clockPlugin)
await fiber
setTimeout(async () => {
console.log('dispose plugin')
await fiber.dispose()
}, 3100)运行:
bash
node index.mjs你会看到类似输出:
text
timer started
tick
tick
tick
dispose plugin
timer stopped这一次,定时器被清理了。
effect 的运行流程
ctx.effect() 做了两件事:
- 执行你传入的函数。
- 收集这个函数返回的清理函数。
在这个模型里,副作用和清理动作是一对。你创建了什么,就应该返回对应的清理函数。
| 副作用 | 清理函数 |
|---|---|
setInterval() | clearInterval() |
| 注册事件监听 | 取消事件监听 |
| 打开数据库连接 | 关闭数据库连接 |
| 启动 HTTP 服务 | 关闭 HTTP 服务 |
| 注册服务 | 移除服务 |
多个 effect 会怎样清理
一个插件可以注册多个 effect。
js
import { Context } from 'cordis'
const ctx = new Context()
function multiEffectPlugin(ctx) {
ctx.effect(() => {
console.log('effect A started')
return () => console.log('effect A stopped')
}, 'effect A')
ctx.effect(() => {
console.log('effect B started')
return () => console.log('effect B stopped')
}, 'effect B')
}
const fiber = ctx.plugin(multiEffectPlugin)
await fiber
await fiber.dispose()你可以把多个 effect 理解成插件的资源清单。插件卸载时,Cordis 会清理这些资源。
实际项目里,不要把所有副作用都塞进一个巨大 effect。更推荐按资源边界拆开:
- 一个 effect 管一个定时器。
- 一个 effect 管一个连接。
- 一个 effect 管一组强相关监听器。
这样出错时更容易定位。
小练习:写一个倒计时插件
需求:
- 插件启动后每秒打印一次剩余秒数。
- 倒计时到 0 后自动清理定时器。
- 外部提前
dispose()时也要清理定时器。
参考实现:
js
import { Context } from 'cordis'
const ctx = new Context()
function countdownPlugin(ctx, config) {
ctx.effect(() => {
let rest = config.seconds
const timer = setInterval(() => {
console.log(`rest: ${rest}`)
rest--
if (rest < 0) {
clearInterval(timer)
console.log('countdown finished')
}
}, 1000)
return () => {
clearInterval(timer)
console.log('countdown disposed')
}
}, 'countdown timer')
}
const fiber = ctx.plugin(countdownPlugin, {
seconds: 3,
})
await fiber
setTimeout(async () => {
await fiber.dispose()
}, 2500)这个练习故意在倒计时结束前卸载插件。你应该能看到清理函数被执行。
入门检查点
记住这一条就够了:
text
插件创建副作用,就要把清理函数交给 ctx.effect()这条纪律是 Cordis 能管理生命周期的基础。下一篇我们继续看事件系统:插件注册事件监听后,为什么卸载时监听器会自动消失。
系列导航:
