这一篇要解决的问题是:当一个模块做完自己的事以后,其他模块也想感知这件事,但双方又不应该直接耦合时,系统该怎么组织。为了把这个问题讲清楚,我们会先手写一个最小事件系统,再看为什么事件本身并不是最难的部分,真正棘手的是 listener 的创建和清理最终一定会把我们带到 Lifecycle。
1. 这一篇要解决的问题
现在增加一个需求:每次调用 greet 之后,都记录一行日志:
[LOG] greeted Alex同时再加一个约束:Greeter 和 App 都不应该知道 Logger 的存在。原因很简单,因为未来也许还会有 Analytics、Alert、Tracing、Metrics 等旁路模块。如果 App 逐渐写成下面这样:
greeter.greet()
logger.log()
analytics.track()
metrics.record()
alert.check()那么随着功能增加,App 会知道越来越多的模块,耦合也会迅速变强。因此,我们需要一种不要求发送方认识接收方的通信方式。
这一篇的主线可以先压缩成下面这条路径:
打完招呼以后还要通知别人
↓
不能直接写死 Logger
↓
引入 Event 做解耦通信
↓
listener 最终并入 Lifecycle2. Vanilla:自己实现一个最小事件系统
在不使用框架的情况下,最直接的做法是维护一个监听器数组:
type GreetListener = (name: string) => void
const greetListeners: GreetListener[] = []
function onGreet(fn: GreetListener) {
greetListeners.push(fn)
}
function offGreet(fn: GreetListener) {
const i = greetListeners.indexOf(fn)
if (i >= 0) greetListeners.splice(i, 1)
}
function emitGreet(name: string) {
for (const fn of greetListeners) fn(name)
}App 在打招呼后发出事件:
function app() {
console.log('[App]', greeter.greet('Alex'))
emitGreet('Alex')
}Logger 订阅这个事件:
function logger(name: string) {
console.log(`[LOG] greeted ${name}`)
}
onGreet(logger)这样一来,App 只负责发事件,Logger 只负责监听事件,两者之间不再需要直接引用。到这里为止,解耦已经成立了。
3. Vanilla:监听器由谁清理?
不过,事件系统很快会引出一个比“能不能发事件”更关键的问题:谁来负责清理监听器?我们虽然写了:
offGreet(logger)但没有任何机制能保证我们将来一定会调用它。假设 Logger 某天被卸载了,如果监听器还留在 greetListeners 数组里,就会留下一个幽灵 listener。问题的关键不在于数组实现得够不够优雅,而在于资源的创建和资源的清理是分离的。我们在某个地方调用了:
onGreet(logger)就必须在未来另一个时刻记得调用:
offGreet(logger)一旦调用者忘记清理,系统本身并不会主动发现。随着事件和模块越来越多,这种遗漏就会变成长时间运行系统里的资源泄漏和重复触发问题。
4. Cordis:把 listener 放进生命周期
在 Cordis 中,先声明 Event 类型:
declare module 'cordis' {
interface Context {
greeter: Greeter
}
interface Events {
greet(name: string): void
}
}App 改成:
function app(ctx: Context) {
console.log('[App]', ctx.greeter.greet('Alex'))
ctx.emit('greet', 'Alex')
}Logger 改成:
function logger(ctx: Context) {
ctx.on('greet', (name) => {
console.log(`[LOG] greeted ${name}`)
})
}安装也很直接:
await ctx.plugin(logger)从表面看,Cordis 只是把事件操作收敛成了:
ctx.emit(...)
ctx.on(...)但它真正重要的地方不在于 API 更短,而在于 ctx.on() 创建出来的 listener 属于当前 Plugin 的生命周期。也就是说,这个监听器不是一个散落在全局数组里的资源,而是跟着 Logger Plugin 一起被管理:
Logger Plugin
│
├── ctx.on('greet', ...)
│
↓
dispose
│
↓
listener 自动清理因此,我们不再需要自己显式维护 offGreet(logger)。这正是 Lifecycle 第一次真正变得具体的地方。
5. 这一篇的结论
这一篇最重要的认识是:一个最小事件系统并不难自己写,真正困难的是把“资源创建”和“资源清理”绑定到同一条生命周期里。Cordis 的价值不是帮你少写一个监听器数组,而是把 Plugin 创建出来的 listener 自动纳入生命周期管理。从这里开始,我们讨论的重点已经不只是“怎么通信”,而是“谁创建资源、谁负责回收资源”。
下一篇会把问题再往前推一步。当 Greeter 不再独立,而是开始依赖一个会动态出现和消失的 Clock 时,系统就不只需要处理事件,还需要开始处理依赖的可用状态。