发布订阅

先理解原理和示例,再练习完整口述与追问。

订阅、精确取消、once、触发时增删、异常策略。

发布订阅把发送者和接收者通过事件中心解耦。实现需要定义同一回调重复订阅是否允许、触发时增删订阅的语义、once 如何在嵌套 emit 下只执行一次、错误是否隔离,以及如何取消。对订阅集合做快照可以减少遍历期间修改的意外行为。

A、B 两个监听,A 执行时取消 B,当前轮是否继续执行 B 要依契约说明;once 监听最好在执行用户回调前解除,避免回调递归 emit 时再次触发。组件卸载调用 off,测试连续订阅/取消后数量不增长。

事件总线隐藏依赖,不适合作为所有共享状态的替代。异常吞掉会掩盖故障,直接中断也可能影响后续监听,应明确策略。

用 Map 保存事件对应监听记录,on 返回取消函数,once 在执行前移除以避免递归触发重复执行。emit 使用开始时快照:新增监听下次生效,本轮尚未执行且已被移除的监听跳过。单个回调抛错不阻止其余回调,最后用 AggregateError 汇总抛出。策略说清楚比只写一个 forEach 更重要。

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

  on(type, fn, once = false) {
    if (typeof fn !== 'function') throw new TypeError('监听必须是函数')
    if (!this.events.has(type)) this.events.set(type, new Set())
    const listeners = this.events.get(type)
    const record = { fn, once }
    listeners.add(record)
    return () => {
      listeners.delete(record)
      if (!listeners.size && this.events.get(type) === listeners) {
        this.events.delete(type)
      }
    }
  }

  once(type, fn) { return this.on(type, fn, true) }

  off(type, fn) {
    const listeners = this.events.get(type)
    if (!listeners) return
    for (const record of listeners) {
      if (record.fn === fn) listeners.delete(record)
    }
    if (!listeners.size) this.events.delete(type)
  }

  emit(type, ...args) {
    const listeners = this.events.get(type)
    if (!listeners) return 0
    const errors = []
    let count = 0
    for (const record of [...listeners]) {
      if (!listeners.has(record)) continue
      if (record.once) listeners.delete(record)
      count++
      try { Reflect.apply(record.fn, undefined, args) }
      catch (error) { errors.push(error) }
    }
    if (!listeners.size && this.events.get(type) === listeners) {
      this.events.delete(type)
    }
    if (errors.length) throw new AggregateError(errors, '监听回调执行失败')
    return count
  }
}

示例:const off=bus.on('save',handler) 后调用 off() 只取消该条记录;bus.off('save',handler) 移除该事件下所有相同函数记录。相同函数可重复订阅。emit 的时间/快照空间为 O(监听数),此总线同步处理回调;异步回调的 Promise 拒绝不在同步 try/catch 捕获范围内,应由调用方处理或另定义 emitAsync。