Skip to content

怎么实现发布订阅? ​

发布订阅用事件名把「发生了什么」和「谁关心」分开。on 注册,off 取消,emit 调用当前监听,once 调用一次后移除。取消时必须用注册时的同一个函数引用。

原理 ​

js
class Emitter {
  constructor() {
    this.events = new Map()
  }

  on(type, handler) {
    const list = this.events.get(type) ?? []
    list.push(handler)
    this.events.set(type, list)
    return () => this.off(type, handler)
  }

  off(type, handler) {
    const list = this.events.get(type)
    if (!list) return
    this.events.set(
      type,
      list.filter((fn) => fn !== handler)
    )
  }

  emit(type, ...args) {
    const list = [...(this.events.get(type) ?? [])]
    for (const handler of list) handler(...args)
  }

  once(type, handler) {
    const wrap = (...args) => {
      this.off(type, wrap)
      handler(...args)
    }
    return this.on(type, wrap)
  }
}

emit 先复制数组再遍历。这样监听函数里面立刻 off 自己或别人,不会改到正在遍历的那一份。

once 包了一层。取消这次一次性监听时,要拿 on 返回的取消函数,或者 off 那个包装函数,而不能 off 当初传入的 handler,因为表里存的是包装。

追问 ​

和观察者模式有什么差别?

观察者模式里,目标通常直接持有观察者。发布订阅中间还有一个事件通道,发布者不必知道订阅者是谁。前端的 event bus、DOM 事件,都是后面这一种。

页面里为什么要少用全局事件总线?

谁都能 emit,数据从哪里来会变得难追。组件之间优先用 props、回调或状态管理。事件总线适合真正的跨模块通知,并且要在卸载时 off。

易错点 ​

  • off 用另一个长得一样的箭头函数,引用不同,取消无效。
  • once 的包装函数如果在 handler 抛错之前就 off,错误不会导致监听留下。上面的实现是先移除再调用;如果希望失败后还能再收一次,顺序要换过来,并在面试里说明。
  • emit 时监听抛错会打断后面的监听。需要隔离的话,每个 handler 各自 try/catch。

相关 ​