事件系统(下):类型安全与作用域

专栏:Cordis 插件框架 · 第 7 / 12 篇
Cordis事件类型

:::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——把”约定”变成”机器可检查”。

事件词汇表的类型约束

图表(cordis-typed-events.md)

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/pluginemit插件 fiber 创建/uid 清除(发布与卸载通知)
internal/statusemitfiber 状态迁移(携带旧状态)
internal/configwaterfall插件配置解析(可改写)
internal/updatewaterfallfiber 配置更新(可否决重启,HMR 的钩子)
internal/get / internal/setwaterfall服务读/写的最后拦截机会
internal/listenerbail监听器注册拦截(dsh 用它实现 update 的全局收集)
internal/dispatchemit事件分发诊断(只对非 internal 事件发)

两处精妙:internal/listener 的 bail 语义让”拦截注册”成为可能——dsh 的 ToolRuntime 用它给工具注册加校验,返回非空值即替换注册行为;internal/get 让”读服务”也可被拦截——代理读取时服务不存在,插件还有最后一次机会现场 provide。

internal/ 事件是框架的自留地:事件名以 internal/ 开头的不会被 internal/dispatch 诊断事件记录(dispatch 里的显式判断),且行为随版本可能变化——插件应优先使用文档化的公共事件,internal 只在读框架源码或明确需要时使用。

常见踩坑

  1. 在 Events 里声明了签名却没导出类型——其他插件 import type 不到你的事件类型,等于放弃了类型安全;
  2. 作用域过滤把全局监听器也滤掉了——需要跨作用域收事件时加 global: true
  3. 在 internal/update 里做了重活——它在每次 fiber 配置更新的 waterfall 路径上同步执行。

随堂练习(带验收标准)

  1. 声明一个带参数与返回值的事件(如 'calc/add'(a: number, b: number): number),验证 emit 时参数类型约束生效;
  2. 写两个 ctx(isolate 同一服务),在父 ctx 上注册无标签监听器与带标签监听器,验证”全局收、本域收”的过滤规则;
  3. 打开 vendor/cordis/src/events.ts:140-156,读懂 internal/listenerinternal/update 的自举注册——框架如何”用自己的事件系统挂自己的钩子”。

← 返回文章列表