发布订阅模式与观察者模式详解

发布订阅模式与观察者模式详解

在前端架构与设计模式相关的面试与实践中,“发布订阅模式(Publish/Subscribe)”与“观察者模式(Observer)”经常被放在一起讨论。很多人会觉得两者“差不多”,但在实现方式和应用场景上,它们还是有本质区别的。

本文主要回答三个问题:

  • 观察者模式与发布订阅模式分别是什么?
  • 两者的差异到底在哪?为什么很多资料会混着说?
  • 在前端工程中,如何实现和使用它们?

一、观察者模式(Observer Pattern)

1. 概念

观察者模式定义了一种“一对多”的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象状态发生变化时,会自动通知所有观察者。

关键点:

  • 主体(Subject) 持有 观察者(Observer)列表
  • 观察者直接 订阅主体,主体状态变更时 主动回调观察者
  • 主体与观察者之间存在直接引用关系

2. 经典结构

1
2
3
4
5
6
7
8
Subject(被观察者)
- observers: Observer[]
- attach(observer)
- detach(observer)
- notify()

Observer(观察者)
- update()

3. 简单实现示例(以 JS 为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Subject {
constructor() {
this.observers = [];
}

attach(observer) {
const exists = this.observers.includes(observer);
if (!exists) {
this.observers.push(observer);
}
}

detach(observer) {
this.observers = this.observers.filter((obs) => obs !== observer);
}

notify(data) {
this.observers.forEach((observer) => observer.update(data));
}
}

class Observer {
constructor(name) {
this.name = name;
}

update(data) {
console.log(`${this.name} 收到更新:`, data);
}
}

// 使用
const subject = new Subject();
const o1 = new Observer("观察者1");
const o2 = new Observer("观察者2");

subject.attach(o1);
subject.attach(o2);

subject.notify({ msg: "状态变化了" });

在这个例子中:

  • Subject 直接持有 Observer 实例的引用
  • 通知时直接调用 observer.update

二、发布订阅模式(Publish/Subscribe Pattern)

1. 概念

发布订阅模式通过“事件通道 / 事件中心”让发布者与订阅者解耦:发布者不会直接感知订阅者的存在,而是将消息发布到某个“频道(topic/event)”,由事件中心负责把消息转发给所有订阅该频道的订阅者。

关键点:

  • 多了一个独立的“事件中心(Event Bus / Broker)
  • 发布者 只关心“往哪个频道发消息”,而不关心谁在监听
  • 订阅者 只关心“订阅哪个频道”,而不关心是谁发布
  • 发布者和订阅者之间完全解耦

2. 典型结构

1
2
3
4
5
6
7
EventBus(事件中心)
- subscribe(topic, handler)
- unsubscribe(topic, handler)
- publish(topic, data)

Publisher(发布者)只调用 EventBus.publish
Subscriber(订阅者)只调用 EventBus.subscribe

3. 简易事件总线实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class EventBus {
constructor() {
this.events = new Map(); // topic -> Set<handler>
}

subscribe(topic, handler) {
if (!this.events.has(topic)) {
this.events.set(topic, new Set());
}
this.events.get(topic).add(handler);
return () => this.unsubscribe(topic, handler); // 返回取消订阅函数
}

unsubscribe(topic, handler) {
const handlers = this.events.get(topic);
if (!handlers) return;
handlers.delete(handler);
if (handlers.size === 0) {
this.events.delete(topic);
}
}

publish(topic, data) {
const handlers = this.events.get(topic);
if (!handlers) return;
handlers.forEach((handler) => handler(data));
}
}

// 使用
const bus = new EventBus();

const off = bus.subscribe("user:login", (user) => {
console.log("订阅者1:用户登录", user);
});

bus.subscribe("user:login", (user) => {
console.log("订阅者2:用户登录", user);
});

bus.publish("user:login", { name: "Sunjc" });

off(); // 取消订阅者1

三、观察者模式 vs 发布订阅模式:核心区别

从结构和耦合度上来看,可以这样总结:

1. 是否存在“中介”

  • 观察者模式
    • 主体(Subject)直接维护观察者列表
    • 主题与观察者存在直接依赖
  • 发布订阅模式
    • 引入第三方“事件中心 / 消息中介”(Event Bus / Broker)
    • 发布者与订阅者 不直接依赖 彼此,只依赖事件中心

2. 通知机制

  • 观察者模式
    • 主体状态变化时,调用 observer.update
    • 通常绑定的是“对象间关系”(如一个 Model 被多个 View 观察)
  • 发布订阅模式
    • 发布者往某个 topic 发布数据
    • 由事件中心调度调用订阅回调
    • 通常绑定的是“事件 / 主题”的概念

3. 使用体验上的差异(伪代码对比)

观察者模式:

1
2
3
subject.attach(observerA);
subject.attach(observerB);
subject.changeState(newState); // 内部 notify 调用 observerA/B

发布订阅模式:

1
2
3
bus.subscribe("topic", listenerA);
bus.subscribe("topic", listenerB);
bus.publish("topic", data); // 事件中心调度 listenerA/B

简单一句话概括:

观察者:被观察者知道“我有这些观察者”;
发布订阅:发布者不知道“有哪些订阅者”,只知道“我发到哪个主题”。


四、前端中的常见应用场景

1. 观察者模式:响应式、数据驱动视图

典型例子:

  • Vue 2 的响应式系统(Object.defineProperty + 依赖收集)
  • 老版本 MVVM 框架中的“数据模型与视图”的绑定

在 Vue 2 中:

  • Dep 管理着一组 watcher(观察者)
  • 当数据变更时,Dep 会通知对应 watcher 更新视图

这本质上就是“观察者模式”的一个具体实现。

2. 发布订阅模式:事件总线(Event Bus)

在前端应用中,经常会使用“事件总线”做跨组件通信:

Vue 2 中的 Event Bus(简化写法)

1
2
3
4
5
6
7
8
9
10
11
// event-bus.js
import Vue from "vue";
export const EventBus = new Vue();

// 组件 A 发布
EventBus.$emit("user:login", user);

// 组件 B 订阅
EventBus.$on("user:login", (user) => {
console.log("收到登录事件", user);
});

这里:

  • 各组件不直接互相依赖
  • 通过 EventBus 这个“事件中心”来解耦

React 中也有类似用法

可以自己实现一个简单的 eventBus,在多个组件中共享,用于:

  • 不适合用 Redux/Context 管理的“局部事件”
  • 跨层级弹窗通知、全局提示等

五、如何选择:什么时候用观察者,什么时候用发布订阅?

可以从几个维度考虑:

1. 对象关系的明确程度

  • 观察者模式 更适合:
    • 某个“主体对象”与“观察者对象”关系明确且稳定,如数据模型与视图
  • 发布订阅模式 更适合:
    • 多模块、跨层级之间的“松散通信”
    • 不希望某个模块知道所有“接收方”的存在

2. 解耦需求

  • 如果你希望 A 与 B 完全解耦,尤其是“许多不同模块都可能关心某类事件”,用发布订阅更好。
  • 如果是“一对多”的强逻辑绑定(一个模型驱动多个视图),用观察者更贴切。

3. 系统复杂度

  • 发布订阅引入了“中间层”,需要维护事件中心,也更容易出现:
    • 事件名称拼写错误不好排查
    • 订阅后忘记取消,导致内存泄漏
  • 观察者模式则更直接,但会在主体与观察者之间形成显式依赖。

六、工程实践建议

  1. 为事件命名制定统一规范(如 模块:动作user:logincart:updated)。
  2. 对于发布订阅模式:
    • 订阅时最好保留取消订阅的句柄,在组件卸载/模块销毁时及时清理。
    • 避免在全局随意创建多个事件中心,最好统一从一个模块导出。
  3. 对于观察者模式:
    • 用于数据与视图之间的绑定时,注意避免过度嵌套依赖,导致调试困难。
  4. 从长远看,当系统规模变大时,可以考虑:
    • 使用更结构化的状态管理/事件流方案(Redux、Vuex/Pinia、RxJS 等),而不仅仅是裸用 EventBus。

七、总结

发布订阅模式与观察者模式既相似又不同:

  • 相同点:本质上都是为了解决“一对多通知”和模块间解耦的问题。
  • 不同点:
    • 观察者模式:主体直接持有观察者列表,主动调用观察者方法;
    • 发布订阅模式:通过事件中心转发消息,发布者与订阅者互不直接感知。

在前端工程中:

  • 响应式系统、数据驱动视图通常更接近“观察者模式”;
  • 事件总线、跨模块事件广播更偏向“发布订阅模式”。

理解二者的差异,并结合具体业务场景选择合适的实现,有助于写出更清晰、可维护性更高的前端架构。


发布订阅模式与观察者模式详解
https://sunjc.vip/2026/02/28/发布订阅模式与观察者模式详解/
作者
Sunjc
发布于
2026年2月28日
许可协议