观察者模式 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、微前端通信
相关推荐
用户21816970493011 小时前
Flutter(四)Dart语法 空安全 运算符 流程控制
前端
许彰午11 小时前
政务督办的分合模式:主办协办的并发审批
前端·javascript·政务
用户693717500138411 小时前
AI时代,程序员该往哪走?
前端·后端
ttwuai11 小时前
GoFrame 后台日志清空失败:无 WHERE 删除为什么被拦住
前端·golang
易筋紫容11 小时前
创建型模式:对象的诞生艺术
开发语言·前端·javascript
Revolution6112 小时前
第一次运行 Node.js:终端里的 JavaScript 怎样执行
后端·面试·node.js
半句唐诗12 小时前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
程序员黑豆12 小时前
鸿蒙应用开发:@Provider 与 @Consumer 跨组件双向同步详解
前端·harmonyos
paopaokaka_luck12 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
进击的程序猿~12 小时前
Go 并发底层原理面试学习指南
开发语言·面试·golang