观察者模式 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 重写,思想不变。

浏览器也内置了观察者:IntersectionObserver、MutationObserver、ResizeObserver 都是------监测目标变化,自动回调。

发布-订阅 → 跨组件通信

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 在 onUnmounted 中 off。

如何选择?

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

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

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

五、总结

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

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

  • 观察者模式:Vue 响应式、IntersectionObserver、MobX、Signals
  • 发布-订阅模式:mitt 事件总线、CustomEvent、微前端通信
相关推荐
MetWeave3 分钟前
一根杆与一张场:把你的机场画上地图
前端·数据可视化
萨小耶4 分钟前
AI 面试答题链路源码分析:Redis 幂等、题目锁与状态机如何配合
人工智能·redis·面试
2601_949575868 分钟前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
Java后端的Ai之路11 分钟前
【AIAgent项目-AI生成PPT-09】灵活排版
前端·vue.js·人工智能·python·powerpoint
MasonYyp15 分钟前
安装npm依赖仓库Verdaccio
前端·npm·node.js
Sailing29 分钟前
【封闭结束3】ReAct 循环:从一次 Function Call 到多步 Agent
人工智能·面试·agent
Antruly29 分钟前
C++ Web 开发选型漫谈:从 Drogon 到 libuvcpp + uvp,八个项目的定位、兼容性与务实排名
开发语言·前端·c++
红红谈说32 分钟前
同一套产品两个技术栈,双版本前端工程差异怎么收敛?
前端·uni-app·vue3·前后端分离·双版本维护·接口约定
不吃鱼的羊37 分钟前
如何建立高自尊
java·服务器·前端
计算机魔术师40 分钟前
编程智能体卡在"需要登录"这步,六种接法代价差得很远
前端