面试题库 词条目录
前端Wiki/面试题库/JavaScript/手写代码题

手写发布订阅与事件总线

发布订阅(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 必须传同一个函数引用是最大实战坑。
🔗 相关词条原型链JavaScriptVue