Core Docs
@skyroc/utils

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');   // ❌ 编译期错误

不传泛型时退化为 EventMapRecord<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-B

API

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 实例。

On this page