前端知库

观察者模式和发布订阅模式有什么区别?前端哪里用到了它们?

观察者模式里,被观察的对象自己保存观察者列表,状态变了直接逐个通知。发布订阅在中间多了一个事件中心,发布者和订阅者互不认识,只按事件名通信。Vue 的响应式依赖收集接近观察者模式,EventEmitter、DOM 事件和全局事件总线是发布订阅。

参考答案

简答

商品降价时要通知所有关注它的用户,这就是“一个东西变了,很多地方要知道”的场景,两种模式都是为它设计的。

观察者模式里,商品自己保存一份关注者列表,提供“添加关注”“取消关注”的方法,价格一变,就遍历列表挨个调用关注者的方法。商品和关注者是直接联系的,商品知道自己通知的是谁。

发布订阅模式在中间加了一个事件中心。订阅者向事件中心说“我关心 price-change 这个事件”,发布者只管向事件中心发出 price-change,由事件中心找到所有订阅者去通知。发布者和订阅者彼此不知道对方存在,只靠事件名联系,所以耦合更松,可以跨模块使用。

前端里两者都很常见。Vue 的响应式数据记住了哪些副作用依赖它,变化时直接触发它们,更接近观察者模式。Node.js 的 EventEmitter、浏览器的 addEventListener、Vue 2 常用的全局事件总线,都是按事件名订阅和触发,属于发布订阅。要注意,订阅了就要在不用时取消,组件卸载时不移除监听,回调和它引用的数据会一直留在内存里。

两种写法对比

下面用同一个降价通知分别实现:

JavaScript
// 观察者模式:被观察者直接持有观察者
class Product {
  #observers = new Set();
  constructor(price) {
    this.price = price;
  }
  subscribe(observer) {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }
  setPrice(price) {
    this.price = price;
    for (const observer of this.#observers) observer.update(this);
  }
}

// 发布订阅:通过事件中心解耦
class EventBus {
  #handlers = new Map();
  on(event, handler) {
    if (!this.#handlers.has(event)) this.#handlers.set(event, new Set());
    this.#handlers.get(event).add(handler);
    return () => this.#handlers.get(event).delete(handler);
  }
  emit(event, payload) {
    for (const handler of this.#handlers.get(event) ?? []) handler(payload);
  }
}

const logs = [];
const phone = new Product(3000);
const unsubscribe = phone.subscribe({ update: (p) => logs.push(`观察者:${p.price}`) });
phone.setPrice(2800);
unsubscribe();
phone.setPrice(2500);

const bus = new EventBus();
bus.on('price-change', (price) => logs.push(`订阅者:${price}`));
bus.emit('price-change', 2800);
bus.emit('stock-change', 0);

console.log(logs.join('|')); // 观察者:2800|订阅者:2800

phone.setPrice(2500) 为什么没有记录?因为在那之前调用了 subscribe 返回的取消函数,观察者已经从列表里删掉了。把取消函数作为返回值,调用方不用自己保存回调的引用,组件卸载时调用一下就能清理。

发布订阅这边,bus.emit('stock-change', 0) 也没有输出。因为没有人订阅这个事件名,事件中心找不到处理函数,什么也不做,发布者并不知道有没有人在听。

怎么选

需要通知的对象明确、关系固定,比如一个数据源对应几个视图,用观察者模式就够了,调用关系清楚,调试时容易顺着找到是谁触发的。

需要跨模块、跨层级通信,发布方不应该依赖接收方,比如埋点、全局消息提示,用发布订阅更合适。可是事件名多了以后,很难知道一个事件被谁发出、被谁接收,所以大型项目里不建议用全局事件总线代替状态管理。手写完整的事件中心,包括 once 和错误处理,见《怎样手写一个支持 on、off、once 和 emit 的事件中心?》。

常见追问

DOM 事件属于哪一种?

addEventListener 按事件类型订阅,dispatchEvent 按类型触发,按事件名解耦,更接近发布订阅。不过事件中心就是元素自己,还多了捕获和冒泡这类传播规则。

Vue 3 为什么去掉了 $on、$off?

Vue 3 移除了实例上的事件接口,官方认为全局事件总线难以追踪数据流,推荐用 props、emit、provide/inject 或状态管理库。确实需要事件总线时,可以用 mitt 这类小库。