DeepSeek Harness插件内核-04:事件总线完美解决各模块通信问题中旨在对EventsService这个为Cordis提供事件总线的基础服务进行介绍,为了避免节外生枝,并没有涉及针对Fiber的介绍。但是Cordis的事件总线和Fiber具有不可分割的关系,我们通过这篇文章从另一个角度对Cordis的事件总线进行重新解读。
1. 从两个_hooks说起
Cordis的EventsService是作为一个单例服务的形式被使用的,所以它的_hooks存储的是一个全局性的事件处理器。如代码所示,_hooks类型为Record<keyof any, Hook[]>,本质上Key和Value类型分别为string | number | symbol和Hook列表的字典。Key可以视为事件名称,Value视为一个有序的事件处理器列表。
typescript
export class EventsService {
_hooks: Record<keyof any, Hook[]> = {}
}
export interface Hook extends EventOptions {
ctx: Context
callback: (...args: any[]) => any
}
export interface EventOptions {
prepend?: boolean
global?: boolean
}
Hook是对EventOptions的扩展,后者指定了两种事件的注册行为:
- prepend :新注册的
Hook是否要至于列表的最前端被优先执行。如果没有显式执行,会添加到事件处理器列表的尾端; - global:是否无条件接收到来自所有Context发出的事件,默认会实施一些过滤逻辑来确定注册的处理器是否执行。
Hook在EventOptions的基础上除了添加作为事件处理函数的callback字段之外,还将事件处理器注册时的Context对象通过ctx字段保存了下来。事件处理函数接受任意输入,也可以返回任意输出。Context具有层次结构,存储在Hook的ctx上的Context表示事件处理器被注册具体哪个层次的Context上。
除了定义在EventsService的_hooks字段上这个表示全局事件处理器的Record<keyof any, Hook[]>对象外,Fiber同样也具有一个同名的_hooks字段。通过如下的代码可知,Fiber的_hooks字段类型为Dict<DisposableList<Function>>,也就是说这个字典的Value不再是一个Hook列表,而直接使用函数列表,只是这个列表类型为DisposableList<Function>。两者有何联系,各自又分别使用在那种场景中呢?
typescript
export class Fiber {
public readonly _hooks: Dict<DisposableList<Function>> = Object.create(null)
}
2. 事件的分发
当Cordis决定触发某个事件时,会根据希望的处理器执行方式调用不同的方法,比如emit、serial、bail和waterfall等(DeepSeek Harness插件内核-04:事件总线完美解决各模块通信问题中提供了针对这些方法的详细介绍),但是最终都会利用如下这个dispatch方法将事件分发出去。
typescript
export class EventsService {
dispatch(type: string, args: any[]) {
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
const name: string = args.shift()
if (!name.startsWith('internal/')) {
this.emit('internal/dispatch', type, name, args, thisArg)
}
const filter = thisArg?.[Context.filter]
return (this._hooks[name] || [])
.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))
.map(hook => hook.callback.bind(thisArg))
}
emit(...args: any[]) {
this.dispatch('emit', args).map(cb => cb(...args))
}
}
这个方法定义了如下的两个方法:
- type :表示分发的模式,
emit、serial、bail和waterfall这四个方法调用dispath方法时,会将各自的方法名作为此参数; - args:分发事件携带的参数列表,或者称为事件的内容荷载。
从dispatch的实现来看,如果args提供的第一个参数是对象或者函数,会作为执行事件处理函数的上下文(this) ,并将第二个参数作为事件名称 ;否则执行事件处理函数的上下文就是null,第一个参数被作为事件名称。接下来它会根据事件名称是否以internal/作为前缀将事件分为内部事件和公共事件,常规事件会通过调用emit方法经历一次额外分发,其目的在于给框架内部(比如调试、日志、开发工具)一个观察所有公共事件分发的钩子 。当emit方法再次调用到dispatch方法时,type参数被设置为emit,args的第一个参数为internal/dispatch,将被作为事件名称。args后面的内容荷载包括:原始的分发类型、事件名称、参数列表和解析出来作为事件处理函数的执行上下文对象。
在根据事件名称从_hooks字典中提取出整个Hook列表后,会经历一个过滤流程,具体过滤逻辑为:
- 如果
Hook的global为true,意味着这是一个全局处理器,无条件符合要求; - 如果解析出来的执行上下文
thisArg对象利用Context.filter(一个预定义的Symbol)定义了针对Context的过滤方法,会以thisArg作为执行上下文,以Hook的ctx作为参数调用此方法,返回true则选择,否则则放弃; - 如果上述过滤方法不存在,等同于默认匹配。
对于最终被选择出来的Hook,直接以thisArg作为上下文调用callback字段返回的事件处理函数。
3. 事件处理器的注册
了解了事件处理器被存放在哪里后,我们接着介绍具体的事件注册或者订阅又具体做了些什么。
3.1 register & unregister
的register方法都做了些什么?该方法的定义如下:
typescript
export class EventsService {
register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
const method = options.prepend ? 'unshift' : 'push'
return this.ctx.fiber.effect(() => {
hooks[method]({ ctx: this.ctx, callback, ...options })
return () => this.unregister(hooks, callback)
}, label)
}
unregister(hooks: Hook[], callback: any) {
const index = hooks.findIndex(hook => hook.callback === callback)
if (index >= 0) {
hooks.splice(index, 1)
return true
}
}
}
register方法定义了如下四个参数:
- label:事件注册对应的诊断标签;
- hooks :用来提供存放事件处理器的
Hook列表; - callback:事件处理函数;
- options :用来控制事件处理器执行顺序(是否需要前置)和过滤规则(是否属于全局处理器)的
EventOptions对象。
options的prepend字段用来决定调用哪个方法(unshift或者push)将生成的Hook对象添加到提供的Hook列表中。本着在Fiber范围内执行的任何具有副作用的操作都可以在dispose方法中被撤销的原则,事件注册应该在当前Fiber被终结点的时候被接触,所以整个事件注册会在当前Fiber对象的effect方法中进行,返回的函数会调用unregister方法解除事件的注册。
3.2 on
register需要提供存放Hook的列表,很明显这只能是一个在Cordis内部调用的方法,我们一般调用EventsService如下这个on方法来注册事件处理函数。
typescript
export class EventsService {
on(name: string | symbol, listener: (...args: any) => any, options?: boolean | EventOptions) {
if (typeof options !== 'object') {
options = { prepend: options }
}
this.ctx.fiber.assertActive()
listener = this.ctx.reflect.bind(listener)
const result = this.bail(this.ctx, 'internal/listener', name, listener, options)
if (result) return result
const hooks = this._hooks[name] ||= []
const label = `ctx.on(${typeof name === 'string' ? JSON.stringify(name) : name.toString()})`
return this.register(label, hooks, listener, options)
}
}
这个方法具有如下三个参数:
- name :通过
string | symbol表示的事件标识; - listener :
dispatch方法在分发事件时调用的处理函数; - options :用来控制事件处理存放位置和过滤条件的
EventOptions对象,如果设置为布尔值,则表示EventOptions对象的prepend字段。
on方法在验证当前Fiber的可用性之后,会调用ReflectService的bind方法将指定的处理函数包装成一个支持动态跟踪当前Context的代理,我的文章DeepSeek Harness插件内核-08:利用RefectService以反射方式扩展Context对此方法有过详细介绍。然后它做了一件非常特别的事情:调用bail方法触发internal/listener事件,把本次事件注册 (name, listener, options) 广播给框架内部。按照bail方法的实现逻辑,如果某个internal/listener监听器返回了值,on就直接返回这个值,走不到下面的常规注册。
3.3 谁在订阅internal/listener事件
internal/listener是一个重要的内部事件,旨在通知又新的事件处理器被注册了,那么究竟谁在监听这个内部事件了。查看EventsService的构造函数,我们会针对该事件的注册。
typescript
export class EventsService {
_hooks: Record<keyof any, Hook[]> = {}
constructor(private ctx: Context) {
...
this.on('internal/listener', function (this: Context, name, listener, options: EventOptions) {
if (name === 'internal/update' && !options.global) {
const hooks = this.fiber._hooks['internal/update'] ??= new DisposableList()
const method = options.prepend ? 'unshift' : 'push'
return hooks[method](listener)
}
})
...
}
}
从注册的事件处理器可以看出,它仅仅对非全局注册(!options.global)条件下的原始事件internal/update进行了拦截,并将指定的事件处理函数添加到当前Fiber的_hooks字典中。Cordis之所以需要专门对internal/update事件进行这样的特殊处理,是因为该事件是在更新插件配置导致重启插件 时发出来的,这个事件只需要在当前Fiber范围内传播 。换言之就是只能执行存储在当前Fiber的_hooks中的处理函数。
typescript
export class EventsService {
_hooks: Record<keyof any, Hook[]> = {}
constructor(private ctx: Context) {
...
this.on('internal/update', function (config, noSave, next) {
const cbs = [...this._hooks['internal/update'] || []]
const _next = () => {
const cb = cbs.shift() ?? next
return cb.call(this, config, noSave, _next)
}
return _next()
}, { global: true, prepend: true })
}
}
export class Fiber {
update(config: any, noSave = false) {
this.assertActive()
this._config = config
if (this.state !== FiberState.ACTIVE) {
this._error = undefined
this._setEpoch(INACTIVE)
this._refresh()
return
}
config = this._resolveConfig(config)
return this.context.waterfall(this, 'internal/update', config, noSave, () => {
this.config = config
this._error = undefined
return this.restart()
})
}
}
internal/update事件的触发发生在Fiber的update方法中。从上面的代码可以看出,该事件是以waterfall形式触发的。那么从Fiber的_hooks中提取函数来处理此事件又是如何实现的呢?这实现在EventsService构造函数中针对internal/update事件的注册。如上面的代码所示,EventsService调用on方法注册的函数会从this._hooks中提取回调函数,这里的this可不是EventsService对象,而是调用waterfall方法发出事件的Fiber对象。
在如下的演示程序中,我们以嵌套的方式在创建的Context上注册了三个插件函数,它们对应的Fiber分别通过变量fiber1、fiber2和fiber3表示。三个嵌套的插件以相同的方式注册了名称分别为internal/update和internal/regular的事件处理器,并在回调函数中输出当前Fiber和事件名称。最后我们以相同的方式在fiber2中调用waterfall方法触发了这两个事件。从输出结果可以看出,internal/update事件仅限于当前Fiber(fiber2)范围内被监听,其他事件(即使是internal事件)能够全局监听。
typescript
import { Context, Fiber } from '@deepseek-ai/cordis'
const ctx = new Context()
let fiber1: Fiber, fiber2: Fiber, fiber3: Fiber
function createListener(label: string, event: string) {
return (...args: any[]) => {
console.log(`[${label}] Receive ${event}`)
const next = args.pop()
return typeof next === 'function' ? next() : undefined
}
}
fiber1 = ctx.plugin((ctx) => {
ctx.events.on('internal/update', createListener('fiber1', 'internal/update'))
ctx.events.on('internal/regular', createListener('fiber1', 'internal/regular'))
fiber2 = ctx.plugin((ctx) => {
ctx.events.on('internal/update', createListener('fiber2', 'internal/update'))
ctx.events.on('internal/regular', createListener('fiber2', 'internal/regular'))
fiber3 = ctx.plugin((ctx) => {
ctx.events.on('internal/update', createListener('fiber3', 'internal/update'))
ctx.events.on('internal/regular', createListener('fiber3', 'internal/regular'))
})
})
})
await fiber1.await()
fiber2!.ctx.events.waterfall(fiber2!,'internal/update',{}, true, ()=>"")
fiber2!.ctx.events.waterfall(fiber2!,'internal/regular',{}, true,()=>"")
输出:
bash
[fiber2] Receive internal/update
[fiber1] Receive internal/regular
[fiber2] Receive internal/regular
[fiber3] Receive internal/regular
其实上面的代码是有问题的,因为它没有它直接将on方法返回的用来解除事件注册的函数丢弃了,这样会导致即使我们调用了Fiber对象的dispose方法,注册的事件依然还在,这相当引入了内存泄漏的问题。正确的写法应该是如下这样:
typescript
import { Context, Fiber } from '@deepseek-ai/cordis'
const ctx = new Context()
let fiber1: Fiber, fiber2: Fiber, fiber3: Fiber
function createListener(label: string, event: string) {
return (...args: any[]) => {
console.log(`[${label}] Receive ${event}`)
const next = args.pop()
return typeof next === 'function' ? next() : undefined
}
}
fiber1 = ctx.plugin((ctx) => {
let dispose1 = ctx.events.on('internal/update', createListener('fiber1', 'internal/update'))
let dispose2 = ctx.events.on('internal/regular', createListener('fiber1', 'internal/regular'))
fiber2 = ctx.plugin((ctx) => {
let dispose1 = ctx.events.on('internal/update', createListener('fiber2', 'internal/update'))
let dispose2 = ctx.events.on('internal/regular', createListener('fiber2', 'internal/regular'))
fiber3 = ctx.plugin((ctx) => {
let dispose1 = ctx.events.on('internal/update', createListener('fiber3', 'internal/update'))
let dispose2 = ctx.events.on('internal/regular', createListener('fiber3', 'internal/regular'))
return ()=>{
dispose1()
dispose2()
}
})
return ()=>{
dispose1()
dispose2()
}
})
return ()=>{
dispose1()
dispose2()
}
})
await fiber1.await()
fiber2!.ctx.events.waterfall(fiber2!,'internal/update',{}, true, ()=>"")
fiber2!.ctx.events.waterfall(fiber2!,'internal/regular',{}, true,()=>"")
await fiber3!.dispose()
fiber2!.ctx.events.waterfall(fiber2!,'internal/update',{}, true, ()=>"")
fiber2!.ctx.events.waterfall(fiber2!,'internal/regular',{}, true,()=>"")
输出:
bash
[fiber2] Receive internal/update
[fiber1] Receive internal/regular
[fiber2] Receive internal/regular
[fiber3] Receive internal/regular
[fiber2] Receive internal/update
[fiber1] Receive internal/regular
[fiber2] Receive internal/regular
4. 混入Context的方法
其实我们很少会直接使用EventsService,更多的情况是使用Context相应的方法来注册和触发事件。如下面的代码所示,这些方法都是在ReflectService构造函数中调用mixin方法混入当前Context对象的。针对该方法的介绍可以在我的文章DeepSeek Harness插件内核-08:利用RefectService以反射方式扩展Context 中找到。
typescript
export class ReflectService {
constructor(public ctx: Context) {
...
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])
}
}
为了编程上的便利,Cordis还为Context接口声明了如下这个用来注册和触发事件的方法。
typescript
declare module './context.ts' {
export interface Context {
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
parallel<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promise<void>
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
emit<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): void
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
serial<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
bail<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
waterfall<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
}
}