这一篇要解决的问题是:当两个 Plugin 不应该直接互相认识,但其中一个又需要感知另一个做了什么时,Runtime 该怎么组织它们。为了把这个问题讲清楚,我们会先看为什么直接共享 service 不够,再看 Event 为什么不是一个额外外挂的子系统,而是可以直接复用现有 effect 和 scope 机制的一种特殊情况。
1. 这一篇要解决的问题
到上一篇为止,MiniCordis 已经能安装和撤销 Plugin,也能按 scope 精确清理它们带来的副作用。但如果现在想表达这样一个需求:Greeter 每次打招呼后,Logger 想记一笔日志,而 Analytics 也想收到同一个信号,那么仅靠共享 service 其实不太合适。
因为一旦 Greeter 需要直接知道 Logger 或 Analytics 的存在,插件之间就会开始耦合。Producer 必须知道 Consumer 是谁,Consumer 也必须围绕 Producer 暴露的方法组织自己,这和插件化 Runtime 想追求的解耦方向是相反的。
所以这一篇的问题可以压缩成一句话:
如果模块之间不应该互相引用,但仍然要互相通信,Runtime 需要一个什么样的机制?
这对应 mini-cordis 里的 v0.7。
这一篇的主线可以先压缩成下面这条路径:
模块想通信
↓
不能直接互相引用
↓
引入 on / emit
↓
把 listener 也当成 effect 管理2. 上一阶段卡在哪里
如果没有 Event,最直接的办法通常只有两种。第一种是 Greeter 主动依赖 Logger,在 greet() 之后直接调用它;第二种是 Logger 去包裹或代理 Greeter 的行为。这两种做法都会把原本应该独立的两个插件重新绑到一起。
更麻烦的是,即便勉强造出一套监听器结构,只要它没有和前面已经建立好的 scope 体系接起来,就会马上出现 cleanup 问题。Logger 被卸载以后,它注册的 listener 到底由谁删掉?如果还需要额外手写 off(),那就等于又回到了上一篇刚刚想避免的状态:setup 和 cleanup 被拆开了。
所以 Event 真正要解决的并不只是“怎么广播消息”,而是更具体的:
怎样让 Producer 和 Consumer 解耦?
怎样让 listener 在 Plugin 卸载时自动消失?3. 这一篇引入什么机制
MiniCordis 这一阶段增加的表面 API 很简单:
ctx.on('greet', handler)
ctx.emit('greet', payload)内部只需要一张非常普通的表:
Map<string, Set<Listener>>但这一篇最重要的设计点不在于这张表,而在于 on() 根本不是一个孤立操作,它内部直接调用了 effect()。也就是说,注册 listener 这件事本身就被视作一种 Runtime effect:
on(event: string, listener: Listener): void {
this.effect(() => {
set.add(listener)
return () => {
set.delete(listener)
}
})
}一旦这样设计,前面所有关于 scope 的机制就会自动生效。哪个 Plugin 在安装过程中调用了 ctx.on(),这个 listener 的 cleanup 就会自然归到那个 Plugin 的 scope 里。等 Plugin 被卸载时,listener 也会随之移除,不需要再为 Event 单独发明一套销毁协议。
4. 代码里真正加了什么
从代码角度看,这一阶段新增的内容其实非常集中。除了 Listener 类型和一张 listeners 表,最核心的变化就是:
private listeners = new Map<string, Set<Listener>>()
on(event: string, listener: Listener): void {
this.effect(() => {
let set = this.listeners.get(event)
if (!set) {
set = new Set()
this.listeners.set(event, set)
}
set.add(listener)
return () => {
set!.delete(listener)
}
})
}
emit(event: string, ...args: unknown[]): void {
const set = this.listeners.get(event)
if (!set) return
for (const listener of set) {
listener(...args)
}
}这里真正值得关注的是一种“模型复用”,而不只是“新增了一块功能”。从结果上看,Event 似乎是 Runtime 新长出来的一层能力;但从实现角度看,它其实完全没有脱离现有框架,而是直接把 listener 注册视为一种 effect。换句话说,MiniCordis 并不是在 Event 这里重新发明了一套生命周期管理,而是进一步证明:前面两篇建立的那套模型已经足够通用。
这也是这一篇的核心价值所在。一个 Runtime 之所以强大,往往不是因为它有很多彼此无关的子系统,而是因为它能用同一套抽象接住很多不同类型的问题。
5. 这一篇的结论
这一篇最重要的结论是:Event 在 MiniCordis 里并不是一个与 Plugin、Effect、Scope 平行的新体系,而是建立在它们之上的一种自然延伸。listener 之所以能被自动管理,关键并不在于有一个 Map,而在于 on() 本身已经被放进了 Runtime 的生命周期模型里。
下一篇会把问题再往前推一步。只要再加入一个“某个能力此刻可能不存在,但未来可能出现、消失、再恢复”的需求,系统就会从普通事件通信进入真正的动态依赖管理。