事件系统(下):类型安全与作用域
专栏:Cordis 插件框架 · 第 7 / 12 篇:::info 学习目标 完成本篇后你能够:给项目添加类型安全的事件词汇;解释作用域过滤的实现链路;说出 internal/* 内部事件的用途与风险。 前置:第 6 篇完成。预计时长:50 分钟。 :::
上一篇讲透了分发的”机器”,这篇讲它的”安全带与导航”:事件名的类型约束、参数的类型检查、作用域过滤。
类型安全:Events 接口 + 声明合并
events.ts:44-55 里所有分发方法的签名都是同一个形状:
// vendor/cordis/src/events.ts:44-55(节选)
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
K extends keyof Events 约束事件名必须是 Events 接口的键;Parameters<Events[K]> / ReturnType<Events[K]> 用上一篇的条件类型从函数签名里提取参数与返回值——你在 Events 里怎么声明事件签名,emit/on 的类型就怎么约束。第三方插件向 Events 里声明合并自己的事件(declare module),全项目共享这份词汇表。
dsh 在此之上只做了一件事:用 JSDoc 标注每个事件的分发模式(@mode waterfall),并加 lint 强制 waterfall 监听器必须带尾参 next——把”约定”变成”机器可检查”。
事件词汇表的类型约束
thisArg:作用域过滤的载体
thisArg:作用域过滤的载体
细看分发方法都有一个 thisArg 重载(把 this 作为第一个参数的版本)。这个 this 不是装饰,是作用域过滤的钥匙:
// vendor/cordis/src/events.ts:165-174(节选)
dispatch(type: string, args: any[]) {
const thisArg = typeof args[0] === 'object' || typeof args[0] === 'function' ? args.shift() : null
...
const filter = thisArg?.[Context.filter] // 从载体上取过滤器
return (this._hooks[name] || [])
.filter(hook => hook.global || !filter || filter.call(thisArg, hook.ctx))
...
}
dsh 的 dsh-scope 包给 ctx 定义了 [Context.filter]:监听器注册时所在的作用域标签会记录在 hook 上,分发时用 filter(hook.ctx) 判定”这个监听器是否属于当前作用域链”。效果(dsh 专栏第 10 篇详述):无标签的监听器全局收事件,带标签的只收本作用域与子孙的。global: true 选项可以绕过过滤(第 4 篇 dsh 的 internal/update 就用它保证配置更新事件全局可见)。
internal/* 事件:框架的自言自语
Events 接口的下半部分是一组 internal/ 前缀事件——框架用自己的事件系统实现自己:
| 内部事件 | 模式 | 用途 |
|---|---|---|
internal/plugin | emit | 插件 fiber 创建/uid 清除(发布与卸载通知) |
internal/status | emit | fiber 状态迁移(携带旧状态) |
internal/config | waterfall | 插件配置解析(可改写) |
internal/update | waterfall | fiber 配置更新(可否决重启,HMR 的钩子) |
internal/get / internal/set | waterfall | 服务读/写的最后拦截机会 |
internal/listener | bail | 监听器注册拦截(dsh 用它实现 update 的全局收集) |
internal/dispatch | emit | 事件分发诊断(只对非 internal 事件发) |
两处精妙:internal/listener 的 bail 语义让”拦截注册”成为可能——dsh 的 ToolRuntime 用它给工具注册加校验,返回非空值即替换注册行为;internal/get 让”读服务”也可被拦截——代理读取时服务不存在,插件还有最后一次机会现场 provide。
internal/ 事件是框架的自留地:事件名以 internal/ 开头的不会被 internal/dispatch 诊断事件记录(dispatch 里的显式判断),且行为随版本可能变化——插件应优先使用文档化的公共事件,internal 只在读框架源码或明确需要时使用。
常见踩坑
- 在 Events 里声明了签名却没导出类型——其他插件
import type不到你的事件类型,等于放弃了类型安全; - 作用域过滤把全局监听器也滤掉了——需要跨作用域收事件时加
global: true; - 在 internal/update 里做了重活——它在每次 fiber 配置更新的 waterfall 路径上同步执行。
随堂练习(带验收标准)
- 声明一个带参数与返回值的事件(如
'calc/add'(a: number, b: number): number),验证 emit 时参数类型约束生效; - 写两个 ctx(isolate 同一服务),在父 ctx 上注册无标签监听器与带标签监听器,验证”全局收、本域收”的过滤规则;
- 打开
vendor/cordis/src/events.ts:140-156,读懂internal/listener与internal/update的自举注册——框架如何”用自己的事件系统挂自己的钩子”。