JavaScript - 观察者与发布订阅者模式
April 11, 2025
13 min read
#观察者模式
观察者模式是一种行为型设计模式,它定义了一种 一对多
的依赖关系,当一个对象(被观察者/主题
)的状态发生改变时,其所有依赖者(观察者
)都会收到通知并自动更新。
这种模式在对象间存在一对多关系时非常有用。比如,当一个对象被修改时,会自动通知所有依赖它的对象。观察者模式是 JavaScript 中常用的设计模式之一,特别适用于 事件处理系统
。
可以将观察者模式类比为拍卖场景:拍卖师(被观察者)观察最高标价,然后通知给所有竞价者(观察者)进行新一轮竞价。
#观察者模式的核心组成部分
Subject
(主题/被观察者):维护观 察者列表,提供添加和删除观察者的方法Observer
(观察者):提供一个更新接口,用于接收主题的通知ConcreteSubject
(具体主题):将有关状态存入各个 ConcreteObserver 对象,当自身状态改变时,向观察者发出通知ConcreteObserver
(具体观察者):存储一个指向 ConcreteSubject 对象的引用,实现 Observer 的更新接口
#优缺点分析
优点:- 观察者和被观察者是
抽象耦合
的,降低了系统的耦合度 - 实现简单,主题和观察者之间的关系清晰明确
- 支持
广播通信
,适用于一对多的依赖关系 - 符合
开闭原则
,增加新的观察者无需修改原有代码
- 当观察者较多时,主题的通知可能会导致
性能问题
- 如果观察者和被观察者之间存在
循环依赖
,可能导致系统崩溃 - 观察者模式没有相应的机制让观察者知道所观察的目标对象是
如何变化
的
#代码实现
观察者类JavaScript
class Observer {constructor(name) {this.name = name;}update(data) {console.log(`观察者 ${this.name} 收到了: ${data}`);}}
被观察者类JavaScript
class Subject {constructor() {this.observers = [];}// 添加观察者addObserver(observer) {if (!this.observers.includes(observer)) {this.observers.push(observer);console.log(`${observer.name} 已添加为观察者`);}}// 通知所有观察者notifyAllObservers(data) {this.observers.forEach(observer => observer.update(data));}// 移除观察者removeObserver(observer) {const index = this.observers.findIndex(obs => obs === observer);if (index !== -1) {this.observers.splice(index, 1);console.log(`${observer.name} 已从观察者列表中移除`);}}}
#使用示例
观察者模式使用示例JavaScript
// 创建主题const newsSubject = new Subject();// 创建观察者const reader1 = new Observer('晴天');const reader2 = new Observer('下雨');const reader3 = new Observer('多云');// 添加观察者到主题newsSubject.addObserver(reader1);newsSubject.addObserver(reader2);newsSubject.addObserver(reader3);// 主题状态变化,通知所有观察者newsSubject.notifyAllObservers('重大新闻:JavaScript 2025版本即将发布!');// 输出:// 观察者 晴天 收到了: 重大新闻:JavaScript 2025版本即将发布!// 观察者 下雨 收到了: 重大新闻:JavaScript 2025版本即将发布!// 观察者 多云 收到了: 重大新闻:JavaScript 2025版本即将发布!// 移除一个观察者newsSubject.removeObserver(reader2);// 再次通知newsSubject.notifyAllObservers('最新消息:TypeScript 5.0发布');// 输出:// 观察者 晴天 收到了: 最新消息:TypeScript 5.0发布// 观察者 多云 收到了: 最新消息:TypeScript 5.0发布
#实际应用场景
- DOM事件处理:浏览器的事件系统本质上是观察者模式的实现
Vue
的响应式系统:当数据发生变化时,视图会自动更新React
的状态管理:组件状态变化触发重新渲染- Node.js 的
EventEmitter
:事件驱动编程的基础
#发布订阅者模式
发布/订阅模式是一种消息范式,其中消息的发送者(发布者
)不会将消息直接发送给特定的接收者(订阅者
),而是通过一个中间人(事件通道/调度中心
)来管理消息的分发。
发布者与订阅者不直接通信,而是通过一个第三方组件来进行交互,彼此 完全解耦
。发布者将消息分为不同的 类别/主题
,而订阅者则表达对特定类别的兴趣,只接收自己感兴趣的消息。
可以将发布订阅模式类比为邮件系统:你作为订阅者订阅某个网站的通知,邮件系统充当 发布订阅中心
的角色,而发布者则是你订阅的网站。
#发布订阅模式的核心组成部分
Publisher
(发布者):发布消息到调度中心Subscriber
(订阅者):从调度中心订阅消息EventChannel
(事件通道/调度中心):管理订阅关系,分发消息
#优缺点分析
优点:- 完全解耦发布者和订阅者,双方互不感知对方的存在
- 灵活性高,扩展性好,可以动态添加或移除订阅关系
- 支持
多对多
的复杂通信模式,可以实现更细粒度的消息分发 - 支持
异步通信
,提高系统的响应性和可扩展性
- 引入中间层(调度中心),增加了系统的
复杂度
- 消息传递链路变长,可能会影响系统的
性能
- 由于发布者和订阅者完全解耦,
调试和追踪
问题变得更加困难
#代码实现
事件通道/调度中心JavaScript
class EventChannel {constructor() {// 储存主题和对应的订阅者回调函数this.topics = {};}// 订阅主题subscribe(topic, callback) {if (!this.topics[topic]) {this.topics[topic] = [];}this.topics[topic].push(callback);// 返回取消订阅的函数return {unsubscribe: () => {this.topics[topic] = this.topics[topic].filter(cb => cb !== callback);if (this.topics[topic].length === 0) {delete this.topics[topic];}}};}// 发布消息到指定主题publish(topic, data) {if (!this.topics[topic] || this.topics[topic].length === 0) {return false;}this.topics[topic].forEach(callback => {setTimeout(() => {callback(data);}, 0);});return true;}// 清除特定主题的所有订阅clearTopic(topic) {if (this.topics[topic]) {delete this.topics[topic];return true;}return false;}}
发布者类JavaScript
class Publisher {constructor(eventChannel) {this.eventChannel = eventChannel;this.name = '';}setName(name) {this.name = name;}// 发布消息到特定主题publish(topic, message) {console.log(`${this.name ? this.name + ' ' : ''}发布了主题 [${topic}] 的消息`);this.eventChannel.publish(topic, message);}}
订阅者类JavaScript
class Subscriber {constructor(name) {this.name = name;this.subscriptions = {};}// 订阅主题subscribe(eventChannel, topic) {if (this.subscriptions[topic]) {console.log(`${this.name} 已经订阅了主题 [${topic}]`);return;}const subscription = eventChannel.subscribe(topic, (data) => {console.log(`订阅者 ${this.name} 收到 [${topic}] 的消息: ${data}`);});this.subscriptions[topic] = {eventChannel,subscription};console.log(`${this.name} 成功订阅了主题 [${topic}]`);}// 取消订阅unsubscribe(topic) {if (!this.subscriptions[topic]) {console.log(`${this.name} 没有订阅主题 [${topic}]`);return false;}this.subscriptions[topic].subscription.unsubscribe();delete this.subscriptions[topic];console.log(`${this.name} 取消订阅了主题 [${topic}]`);return true;}}
#使用示例
发布订阅模式使用示例JavaScript
// 创建事件通道const eventChannel = new EventChannel();// 创建发布者const newsPublisher = new Publisher(eventChannel);newsPublisher.setName('新闻社');const weatherPublisher = new Publisher(eventChannel);weatherPublisher.setName('气象局');// 创建订阅者const user1 = new Subscriber('晴天');const user2 = new Subscriber('下雨');const user3 = new Subscriber('多云');// 订阅主题user1.subscribe(eventChannel, 'news');user1.subscribe(eventChannel, 'weather');user2.subscribe(eventChannel, 'news');user3.subscribe(eventChannel, 'weather');// 发布消息newsPublisher.publish('news', '今日头条:JavaScript成为世界上最流行的编程语言');// 输出:// 新闻社 发布了主题 [news] 的消息// 订阅者 晴天 收到 [news] 的消息: 今日头条:JavaScript成为世界上最流行的编程语言// 订阅者 下雨 收到 [news] 的消息: 今日头条:JavaScript成为世界上最流行的编程语言weatherPublisher.publish('weather', '北京今日天气:晴,25°C');// 输出:// 气象局 发布了主题 [weather] 的消息// 订阅者 晴天 收到 [weather] 的消息: 北京今日天气:晴,25°C// 订阅者 多云 收到 [weather] 的消息: 北京今日天气:晴,25°C// 取消订阅user1.unsubscribe('news');// 输出:// 晴天 取消订阅了主题 [news]// 再次发布新闻newsPublisher.publish('news', '最新消息:TypeScript 5.0发布');// 输出:// 新闻社 发布了主题 [news] 的消息// 订阅者 下雨 收到 [news] 的消息: 最新消息:TypeScript 5.0发布
#实际应用场景
- 前端框架中的
事件总线
:Vue 中的 EventBus,用于组件间的通信 消息队列系统
:RabbitMQ、Kafka 等消息中间件- 浏览器的
自定义事件
:使用 CustomEvent API 进行复杂事件通信 Redux/Vuex
等状态管理库:通过 action/mutation 发布状态变更
#观察者模式与发布订阅模式的区别
虽然这两种模式经常被混淆,但它们有几个关键区别:
-
组件关系:
- 观察者模式:观察者与被观察者直接交互,存在
直接的依赖关系
- 发布订阅模式:发布者和订阅者完全解耦,通过中间的
事件通道
通信
- 观察者模式:观察者与被观察者直接交互,存在
-
消息传递方式:
- 观察者模式:被观察者
直接调用
观察者的更新方法进行通知 - 发布订阅模式:发布者不知道消息会被谁接收,只负责发布到特定
主题
- 观察者模式:被观察者
-
关注点:
- 观察者模式:观察者关注特定的
被观察者对象
- 发布订阅模式:订阅者关注特定的
事件/主题/频道
- 观察者模式:观察者关注特定的
-
灵活性:
- 观察者模式:耦合度相对较高,扩展性较差
- 发布订阅模式:
完全解耦
,扩展性更好,可以实现更复杂的消息路由
下图直观地展示了两种模式的区别:
观察者模式: 发布订阅模式:+-----------+ +-----------+| Subject | | Publisher |+-----------+ +-----------+| || 通知 | 发布▼ ▼+-----------+ +-----------+| Observer | ◄-X-X-X-X-X-X-> | EventBus |+-----------+ +-----------+|| 通知▼+-----------+|Subscriber |+-----------+
#总结
观察者模式和发布订阅模式都是处理对象间通信的有效设计模式,但 它们适用于不同的场景:
- 观察者模式:当系统中存在明确的
一对多依赖关系
,且对象间的通信比较简单直接时,应考虑使用。 - 发布订阅模式:当系统需要
高度解耦
,或需要处理复杂的消息路由、过滤和分发时,应考虑使用。
选择哪种模式取决于系统的需求,特别是在考虑 耦合度
、扩展性
和 性能
之间的权衡时。在实际开发中,可以根据具体情况选择合适的模式,甚至可以结合两种模式的优点来设计更灵活的事件系统。