Emitter
轻量级类型安全事件总线,支持泛型事件映射、通配符监听、粘性事件与键控隔离
概述
Emitter 是一个轻量级的发布/订阅实现。与标准 Node.js EventEmitter 相比,它额外提供了四项能力:
- 泛型事件映射 — 在 TypeScript 编译期检查事件名与参数类型,拼错事件名或传错参数时立即报错
- 通配符
*— 注册一个监听器接收所有事件,常用于日志、调试 - 粘性事件(Sticky Events) — 如果某事件触发时尚无任何监听器,参数会被暂存;晚注册的监听器在
on()时会立即收到这些积压的调用 - 键控事件(Map 模式) — 同一事件名下按自定义
key隔离监听器,适合"按行 / 按组件实例"区分推送
快速上手
import { Emitter } from '@skyroc/utils';
// 定义事件映射:事件名 → 参数元组
type AppEvents = {
login: [user: string, timestamp: number];
logout: [];
notification: [message: string];
};
const bus = new Emitter<AppEvents>();
// 注册监听器,返回取消订阅函数
const off = bus.on('login', (user, timestamp) => {
console.log(`${user} 登录于 ${timestamp}`);
});
// 触发事件
bus.emit('login', 'alice', Date.now()); // ✅ 类型正确
bus.emit('login', 123); // ❌ 编译期类型错误
// 取消订阅
off();核心特性
泛型事件映射
传入泛型参数后,emit / on 的类型会自动收窄:
type Events = {
resize: [width: number, height: number];
close: [];
};
const bus = new Emitter<Events>();
bus.on('resize', (w, h) => {}); // w: number, h: number
bus.on('close', () => {}); // 无参数
bus.emit('resize', 800, 600); // ✅
bus.emit('close', 'extra-arg'); // ❌ 编译期错误不传泛型时退化为 EventMap(Record<string, unknown[]>),与旧代码完全兼容:
const bus = new Emitter(); // 无泛型,向后兼容
bus.on('any-event', (...args) => console.log(args));
bus.emit('any-event', { name: 'Alice' });通配符监听
用 '*' 注册的监听器会在所有事件触发时被调用,第一个参数是事件名:
// 适合日志、调试、监控场景
const offAll = bus.on('*', (eventName, ...args) => {
console.log('[bus]', eventName, args);
});
bus.emit('login', 'alice', Date.now()); // 触发 '*' 监听器
offAll(); // 取消注意: 通配符监听器存在时,未被任何具名监听器监听的事件不会被存为粘性事件。
粘性事件
如果事件触发时没有任何具名监听器,参数会被暂存。之后调用 on() 注册监听器时,积压的调用会立即同步执行:
const bus = new Emitter<{ ready: [config: object] }>();
// 先触发,此时无任何监听器
bus.emit('ready', { theme: 'dark' });
// 500ms 后注册监听器
setTimeout(() => {
// on() 调用时立即收到上面那次触发的参数
bus.on('ready', (config) => {
console.log('收到粘性事件:', config); // { theme: 'dark' }
});
}, 500);适合「初始化顺序不确定」的场景:生产者先 emit,消费者稍后 on,不会漏掉消息。
键控事件(Map 模式)
onMap / emitMap 在事件名的基础上再加一层 key 隔离,适合「同一事件,按实例区分监听器」的场景:
type Events = {
update: [data: Record<string, unknown>];
};
const bus = new Emitter<Events>();
// 两个组件实例各自只监听属于自己 key 的 update
bus.onMap('update', 'panel-A', (data) => console.log('A:', data));
bus.onMap('update', 'panel-B', (data) => console.log('B:', data));
bus.emitMap('update', 'panel-A', { value: 1 }); // 只触发 panel-A
bus.emitMap('update', 'panel-B', { value: 2 }); // 只触发 panel-BAPI
emit(event, ...args)
触发事件,通知所有具名监听器和通配符监听器。
bus.emit('login', 'alice', Date.now());emitMap(event, key, ...args)
触发键控事件,只通知对应 key 下注册的监听器。
bus.emitMap('update', 'panel-A', { value: 1 });on(event, fn)
注册监听器,返回取消订阅函数。注册时会消费该事件积压的粘性事件。
const off = bus.on('login', (user) => {});
off(); // 取消订阅| 重载 | 说明 |
|---|---|
on('*', fn) | 通配符,监听所有事件 |
on(event, fn) | 具名事件监听 |
onMap(event, key, fn)
注册键控监听器,不返回取消函数(需用 offMap 手动取消)。
bus.onMap('update', 'panel-A', handler);off(event, fn?)
移除监听器。不传 fn 则移除该事件的所有监听器。
bus.off('login', handler); // 移除指定监听器
bus.off('login'); // 移除 login 的全部监听器
bus.off('*', wildcardFn); // 移除通配符监听器offMap(event, key, fn)
移除指定 key 下的某个键控监听器。
bus.offMap('update', 'panel-A', handler);offAll()
清除所有普通监听器、键控监听器和粘性事件缓存。常用于实例销毁时的全量清理。
bus.offAll();设计说明
为什么要有粘性事件?
在异步初始化场景中,生产者(如配置加载完成后 emit)可能早于消费者(组件 mount 后 on)执行。粘性事件让生产者不必等待消费者存在,消费者也不必关心自己是否"来晚了"。
为什么要有 Map 模式?
当同一个全局 bus 被多个组件实例共用时,普通的 on/emit 会广播给所有监听器。Map 模式提供了轻量的"按 key 路由"能力,无需为每个实例创建独立的 bus 实例。