Skip to content

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 能管理生命周期的基础。下一篇我们继续看事件系统:插件注册事件监听后,为什么卸载时监听器会自动消失。


系列导航