标准答案

  1. 观察者模式通常是被观察者直接维护观察者列表,状态变化时直接通知观察者。被观察者和观察者之间存在直接关系。
  2. 发布订阅模式中间多了一个事件中心。发布者只负责发布事件,订阅者只负责订阅事件,双方不需要直接知道彼此,因此解耦更强。
  3. EventEmitter 的核心结构是事件名到监听器集合的映射。on 注册监听器,off 移除监听器,emit 按事件名触发监听器并传递参数,once 注册只执行一次的监听器。
  4. 实现时要处理多个监听器、重复注册、解绑引用、once 包装函数、emit 过程中增删监听器等边界。更完整的版本还会处理错误事件、最大监听数量和异步监听器。

题目解析

观察者模式更像“对象直接通知自己的观察者”,发布订阅更像“所有消息先经过事件中心”。这也是它们在耦合程度上的主要差异。

Map<string, Set<Function>> 是比较直接的数据结构。Map 保存事件名,Set 保存监听器,Set 能避免同一个函数被重复加入同一个事件。

emit 时最好遍历监听器快照,而不是直接遍历原集合。这样监听器内部执行 off 或 on 时,不会影响本轮事件触发的遍历结果。

代码示例

下面是一个包含 on、off、emit、once 的核心版 EventEmitter。

JavaScript
class EventEmitter {
  constructor() {
    this.events = new Map()
  }

  on(type, handler) {
    const handlers = this.events.get(type) || new Set()
    handlers.add(handler)
    this.events.set(type, handlers)

    return () => this.off(type, handler)
  }

  off(type, handler) {
    const handlers = this.events.get(type)
    if (!handlers) return

    for (const item of handlers) {
      if (item === handler || item.original === handler) {
        handlers.delete(item)
      }
    }

    if (handlers.size === 0) {
      this.events.delete(type)
    }
  }

  emit(type, ...args) {
    const handlers = this.events.get(type)
    if (!handlers) return false

    Array.from(handlers).forEach(handler => handler(...args))
    return true
  }

  once(type, handler) {
    const wrapper = (...args) => {
      this.off(type, wrapper)
      handler(...args)
    }

    wrapper.original = handler
    return this.on(type, wrapper)
  }
}

常见误区

  • 只实现 on/emit,漏掉 off 和 once。
  • once 包装后无法正确 off 原回调。
  • emit 时直接遍历原数组,导致回调中增删监听器影响当前遍历。

作者信息