观察者模式 vs 发布-订阅模式:从概念到实战,一次讲清楚

0 阅读3分钟

观察者模式(Observer Pattern)和发布-订阅模式(Pub-Sub Pattern)是前端开发中最常见、也最容易被混为一谈的两种设计模式。本文用同一个业务场景实现两种模式,对比核心差异,给出前端落地实践。

Slide 4_3 - 28.png

一、一个需求,两种实现

假设你在做股票行情面板,WebSocket 实时推送价格,页面上的图表、观察列表、日志模块都需要响应这些数据。WebSocket 不需要知道谁在消费这些数据,它只需要说“新行情来了”,让感兴趣的各方自行处理。

1. 观察者模式实现

观察者模式中,被观察者(Subject)直接持有观察者列表,状态变化时主动逐一通知

class Subject {
  #observers = new Set();

  subscribe(observer) {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }

  notify(payload) {
    this.#observers.forEach(observer => observer(payload));
  }
}

使用:

const ticker = new Subject();

const drawChart = (tick) => { /* 重绘图表 */ };
const flashRow = (tick) => { /* 闪烁行情行 */ };
const logTick = (tick) => { /* 记录日志 */ };

ticker.subscribe(drawChart);
ticker.subscribe(flashRow);
ticker.subscribe(logTick);

// WebSocket 推送时
socket.addEventListener("message", (event) => {
  ticker.notify(JSON.parse(event.data));
});

特点ticker 直接持有所有消费者的引用,主动调用它们。新增消费者只需要 subscribe,改消费者只需要改对应的回调函数——但所有消费者都收到每次通知。

2. 发布-订阅模式实现

发布-订阅模式中,发布者和订阅者完全解耦,通过事件中心转发消息

class EventBus {
  #events = {};

  on(event, callback) {
    (this.#events[event] ||= []).push(callback);
    return () => this.off(event, callback);
  }

  off(event, callback) {
    this.#events[event] = this.#events[event]?.filter(cb => cb !== callback);
  }

  emit(event, ...args) {
    this.#events[event]?.forEach(cb => cb(...args));
  }
}

使用:

const bus = new EventBus();

bus.on('tick', drawChart);
bus.on('tick', flashRow);
bus.on('tick', logTick);

socket.addEventListener("message", (event) => {
  bus.emit('tick', JSON.parse(event.data));
});

特点:发布者不直接调用消费者,而是向事件中心发布消息。可以按主题区分('tick/AAPL''tick/GOOG'),不同模块只订阅自己关心的主题。

二、前端落地:各自用在哪?

观察者模式 → Vue 响应式

Vue 2 中每个响应式属性都有一个 Dep(被观察者),组件的渲染函数封装成 Watcher(观察者)。读取属性时收集依赖,修改属性时派发更新。Vue 3 用 Proxy 重写,思想不变。

浏览器也内置了观察者:IntersectionObserverMutationObserverResizeObserver 都是——监测目标变化,自动回调。

发布-订阅 → 跨组件通信

React 和 Vue 3 中,跨多层组件通信常用 mitt(200 字节的发布-订阅库):

import mitt from 'mitt';

type Events = { 'user-login': { userId: string; name: string } };
export const eventBus = mitt<Events>();

// 发布者(Login.tsx)
eventBus.emit('user-login', { userId: user.id, name: user.name });

// 订阅者(Navbar.tsx)
useEffect(() => {
  const handler = (data) => setUser(data);
  eventBus.on('user-login', handler);
  return () => eventBus.off('user-login', handler); // 必须取消订阅
}, []);

微前端中 qiankun 的 initGlobalState 底层也是发布-订阅,主应用和子应用通过事件中心通信。

三、共同的坑与选择建议

最大陷阱:内存泄漏。两种模式都依赖订阅/观察机制,组件卸载时必须取消订阅,否则回调持有的上下文无法被 GC 回收。React 在 useEffect 返回清理函数中 off,Vue 在 onUnmountedoff

如何选择?

场景推荐模式
数据变化驱动视图更新(Vue 响应式)观察者
跨多级组件通信、全局通知发布-订阅
监听 DOM 变化观察者(浏览器 API)
模块间按主题区分消息发布-订阅

有明确的数据源和消费者依赖关系 → 观察者;

两个模块只是因为恰巧对同一事件感兴趣 → 发布-订阅。

五、总结

观察者模式和发布-订阅模式是前端开发中最有价值的两种行为型设计模式。它们的核心区别在于有没有中间的事件中心——这个差异决定了它们分别适合“精准的一对多依赖”和“松耦合的模块间通信”。

记住它们各自的前端落地场景:

  • 观察者模式:Vue 响应式、IntersectionObserver、MobX、Signals
  • 发布-订阅模式:mitt 事件总线、CustomEvent、微前端通信