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

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

一、一个需求,两种实现

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

1. 观察者模式实现

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

javascript 复制代码
class Subject {
  #observers = new Set();

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

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

使用:

javascript 复制代码
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. 发布-订阅模式实现

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

javascript 复制代码
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));
  }
}

使用:

javascript 复制代码
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 字节的发布-订阅库):

typescript 复制代码
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、微前端通信
相关推荐
暗不需求1 小时前
手写 AJAX:从 XMLHttpRequest 到封装一个简易 axios
前端·面试
cxxcode1 小时前
CSS Loader 与样式处理链路
前端·css
电商API_180079052471 小时前
电商ERP 自动同步订单功能实现拆解与技术手段
服务器·前端·网络·爬虫
cxxcode1 小时前
CSS 隔离与 qiankun 样式问题总结
前端
__sjfzllv___1 小时前
在职前端Leader学习/转行 AI Agent -DAY3
前端
不负岁月无痕1 小时前
STL -- C++ 红黑树封装 Map 和 Set
java·c语言·开发语言·c++·面试
miss1 小时前
海康威视 IoT 平台前端实战:视频流、海量设备与实时数据可视化
前端·javascript·物联网
mONESY1 小时前
MCP 协议实战博客:跨进程、跨语言可复用 AI 工具标准化方案
javascript
用户2849709525571 小时前
基于MQTT+SpringBoot的实验室设备数据采集与实时监控系统实战
前端