观察者模式(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、微前端通信