事件总线(EventBus)是前端解耦组件通信的常见方案:一处 emit,多处 on 监听。难点在于"事件名与回调参数一一对应"——纯 JS 实现里 emit 时传错参数、on 时写错事件名都不会报错,运行时才暴露问题。本 demo 用映射类型把事件名映射到参数类型,让 emit、on、off 都被类型系统强约束,事件名拼错或参数类型不符都会编译期报错。
// 事件总线类型:事件名与参数类型一一映射
// emit / on / off 全部由类型系统强约束
// 1. 事件映射表:键为事件名,值为回调参数类型
interface EventMap {
login: { userId: string; token: string };
logout: undefined;
message: { from: string; content: string };
notify: string;
}
// 2. 回调类型:无参事件用 () => void,有参事件用 (payload) => void
type Handler<P> = [P] extends [undefined] ? () => void : (payload: P) => void;
// 3. 事件总线类:泛型约束事件映射表
class EventBus<M extends Record<string, unknown>> {
private handlers: { [K in keyof M]?: Set<Handler<M[K]>> } = {};
// on:事件名必须是 M 的键,回调参数类型自动推导
on<K extends keyof M>(event: K, handler: Handler<M[K]>): this {
(this.handlers[event] ??= new Set()).add(handler);
return this;
}
// off:取消订阅,签名与 on 一致
off<K extends keyof M>(event: K, handler: Handler<M[K]>): this {
this.handlers[event]?.delete(handler);
return this;
}
// emit:无参事件可不传参,有参事件参数类型必须匹配
emit<K extends keyof M>(...args: [undefined] extends [M[K]] ? [event: K] : [event: K, payload: M[K]]): void {
const set = this.handlers[args[0]];
if (!set) return;
const payload = args.length > 1 ? args[1] : undefined;
set.forEach(function (h) { (h as Function)(payload); });
}
}
// 应用:创建强类型事件总线实例
const bus = new EventBus<EventMap>();
bus.on('login', function (p) { // p 自动推导为 { userId: string; token: string }
console.log(p.userId, p.token);
});
bus.on('logout', function () { // 无参事件回调不带参数
console.log('已退出登录');
});
bus.emit('login', { userId: 'u_1', token: 'tk' }); // 参数类型匹配
bus.emit('logout'); // 无参事件无需传 payload
EventMap:用接口把事件名与参数类型绑定,是整个类型约束的源头;后续 on/off/emit 的泛型 K 都从它的 keyof 推导。Handler<P> 条件类型:用 [P] extends [undefined] ? () => void : (payload: P) => void 区分无参事件(logout)与有参事件(login),让无参事件回调不带参数更符合直觉。emit 参数条件分发:用 [undefined] extends [M[K]] ? [event] : [event, payload] 控制可变参数签名,无参事件 emit('logout') 合法,有参事件必须带 payload。M extends Record<string, unknown>:约束事件映射表必须是对象类型,M[K] 索引访问拿到每个事件对应的参数类型,贯穿 on/off/emit。??= 空值合并赋值:订阅时懒初始化对应事件的 Set,避免每个事件都预创建集合,是运行时层面的小优化。// 事件名拼错会被编译期拦截
// bus.emit('logn', { userId: 'u_1', token: 'tk' });
// 报错:logn 不在 EventMap 的键中
// payload 类型不符会被拦截
// bus.emit('login', { userId: 'u_1' });
// 报错:缺少 token 字段
// 无参事件多传参数会被拦截
// bus.emit('logout', 'extra');
// 报错:logout 期望 0 个参数
// notify 事件参数为 string
bus.on('notify', function (msg) { // msg 自动推导为 string
console.log('通知:' + msg);
});
bus.emit('notify', '系统维护中');
// 链式调用:on 返回 this
bus.on('message', function (m) { console.log(m.from, m.content); })
.on('message', function (m) { console.log('第二条订阅'); });
// off 取消单个订阅
const cb = function (m: { from: string; content: string }) { console.log(m); };
bus.on('message', cb);
bus.off('message', cb);