第 1 卷
张嘉熙 · DeepSeek Harness 前沿工程实践



你好,我是张嘉熙。
前两讲我们打通了时间(可撤销 Effect)和空间(响应式依赖)两个维度。这一讲补上第三块拼图——通信。前面讲的依赖解决的是“要”(A 需要 B 的能力)的问题,但插件之间还有一
种关系是“说”(A 要通知 B 发生了某件事)。比如用户发出一个事件,日志插件要记录、统计插件要计数——事件发出方和事件接收方没有直接依赖关系,只是这些插件都想“听到”这件
事。
Cordis 中通信的核心在于它的事件系统,它把事件分发这件事,拆成了五种模式。
我们先看看 Cordis 中事件系统的基本使用方式:
复制代码 1// 来自官方 cordis-tutorial 第四章的 stats.ts(节选)
7
| 8 | exportclassStatsServiceextendsService{ |
|---|---|
| 9 | bump(name:string) { |
| 10 | // ...* |
| 11 | this.ctx.emit('stats/report', name, next) //发出事件,此处的next为当前count自增 |
| 12 | } |
| 13 | } |
复制代码
| 1 | //来自官方cordis-tutorial第四章的reporter.ts(节选) |
|---|---|
| 2 | exportfunctionapply(ctx: Context) { |
| 3 | ctx.on('stats/report',(name, count) =>{ //监听事件 |
| 4 | console.log(`[stats]${name}->${count}`) |
| 5 | }) |
| 6 | } |
这里我们已经能看到事件系统的两个核心动作,以及一种似曾相识的声明方式:
- emit(发出)和 on(监听):一方 emit('stats/report', ...) 发出,另一方
on('stats/report', ...) 监听。事件名用 namespace/action 的斜杠形式,因为所有事件共享一个扁平命名空间,斜杠前缀(stats/)能把相关事件分组。
- declare module { interface Events } 这和上一讲的声明合并很类似——它把
「事件名 + 监听器签名」写进声明,让 emit/on 都获得类型检查(写错编译期就报错)。
下面我们直接跑起来,逐个拆解这五种分发模式。
emit 与 parallel——通知与通知并等待
这两个模式都用于广播,你不想从监听器那里拿回什么,只是想告诉它们事情发生了。区别在于要不要等它们做完。
3})
4ctx.on('chat/message', (text) => {
| 5 | console.log(' | [统计插件]消息字数:', text.length) |
|---|---|---|
| 6 | }) | |
| 7 | ctx.emit('chat/message','你好,Cordis') |
emit 是同步的:它按注册顺序逐个调用监听器,不等待(即使监听器返回 Promise 也不管),不收集返回值。所以,`emit` 适合“发通知”这类 fire-and-forget 的场景。
顺便提一下:除了 ctx.on,Cordis 还有 ctx.once——监听器在首次被触发后自动注销,适合只关心下一次事件的场景,比如等连接建立就执行一次初始化。两者都返回一个
disposer,且都随插件卸载自动移除。
parallel:并发执行,一起等待
复制代码 1ctx.on('task/run', async (name) => {
2await new Promise(r => setTimeout(r, 100))// 模拟耗时任务 A*console.log('[任务 3})
4ctx.on('task/run', async (name) => { 5await new Promise(r => setTimeout(r, 100))// 模拟耗时任务 B*console.log('[任务
6})
7await ctx.parallel('task/run', '并行任务')
parallel 会 await 所有监听器,而且它们是并发跑的。当你有多个互不依赖的耗时任务(比如同时调用多个外部 API),用 parallel 能省下大量时间。注意:如果任何一个监听
器抛错,parallel 会抛出一个 AggregateError 汇总所有错误。
一句话区分:emit 是我通知你们,但我没空等;parallel 是我通知你们,并且等你们都做完。