手写发布订阅与事件总线
发布订阅(pub / sub)是前端最常见的解耦模式:发布者不直接调用订阅者,而是把消息丢给事件中心,由事件中心转发。DOM 事件、Vue 的 $emit / $on、Node 的 EventEmitter、消息队列都是它的影子。它和“观察者模式”的区别在于:观察者是目标对象直接维护观察者列表,发布订阅多了一个中间的事件中心。
手写 EventEmitter 的要点:用 Map<事件名, 回调数组> 存订阅;on 注册、emit 遍历触发、off 精确移除(需保存同一个函数引用)、once 用包一层 + 自动移除。注意箭头函数匿名订阅会导致 off 无法移除 —— 这是实战中最常见的坑。
- 发布订阅 = 事件中心转发消息,发布者与订阅者互不认识
- 观察者模式由目标直接维护观察者列表,发布订阅多一个中间件
- off 移除必须传同一个函数引用,匿名箭头函数订阅无法移除
JS
// 手写事件总线(面试标答) class EventBus { constructor() { this.events = new Map(); } on(name, cb) { // 订阅 if (!this.events.has(name)) this.events.set(name, []); this.events.get(name).push(cb); } emit(name, ...args) { // 发布:遍历触发 const cbs = this.events.get(name); if (cbs) cbs.forEach(cb => cb(...args)); } off(name, cb) { // 移除:需同一个函数引用 const cbs = this.events.get(name); if (!cbs) return; if (cb) this.events.set(name, cbs.filter(f => f !== cb)); else this.events.delete(name); // 不传 cb:清空该事件 } once(name, cb) { // 只触发一次 const wrap = (...args) => { cb(...args); this.off(name, wrap); }; this.on(name, wrap); } } // 验证 const bus = new EventBus(); const greet = (who) => console.log('hello,', who); bus.on('greet', greet); bus.once('greet', () => console.log('我只触发一次')); bus.emit('greet', '小明'); // hello, 小明 / 我只触发一次 bus.emit('greet', '小红'); // hello, 小明 再输出一次?见下 bus.off('greet', greet); bus.emit('greet', '小刚'); // hello, 小刚(greet 已移除) console.log('最终订阅者数:', bus.events.get('greet').length); // 0
本节要点EventBus = Map<事件, 回调数组> + on/emit/off/once;off 必须传同一个函数引用是最大实战坑。




