JavaScript - 观察者与发布订阅者模式

#观察者模式

观察者模式是一种行为型设计模式,它定义了一种 一对多 的依赖关系,当一个对象(被观察者/主题)的状态发生改变时,其所有依赖者(观察者)都会收到通知并自动更新。

这种模式在对象间存在一对多关系时非常有用。比如,当一个对象被修改时,会自动通知所有依赖它的对象。观察者模式是 JavaScript 中常用的设计模式之一,特别适用于 事件处理系统

可以将观察者模式类比为拍卖场景:拍卖师(被观察者)观察最高标价,然后通知给所有竞价者(观察者)进行新一轮竞价。

#观察者模式的核心组成部分

  1. Subject(主题/被观察者):维护观察者列表,提供添加和删除观察者的方法
  2. Observer(观察者):提供一个更新接口,用于接收主题的通知
  3. ConcreteSubject(具体主题):将有关状态存入各个 ConcreteObserver 对象,当自身状态改变时,向观察者发出通知
  4. 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发布

#实际应用场景

  1. DOM事件处理:浏览器的事件系统本质上是观察者模式的实现
  2. Vue 的响应式系统:当数据发生变化时,视图会自动更新
  3. React 的状态管理:组件状态变化触发重新渲染
  4. Node.js 的 EventEmitter:事件驱动编程的基础

#发布订阅者模式

发布/订阅模式是一种消息范式,其中消息的发送者(发布者)不会将消息直接发送给特定的接收者(订阅者),而是通过一个中间人(事件通道/调度中心)来管理消息的分发。

发布者与订阅者不直接通信,而是通过一个第三方组件来进行交互,彼此 完全解耦。发布者将消息分为不同的 类别/主题,而订阅者则表达对特定类别的兴趣,只接收自己感兴趣的消息。

可以将发布订阅模式类比为邮件系统:你作为订阅者订阅某个网站的通知,邮件系统充当 发布订阅中心 的角色,而发布者则是你订阅的网站。

#发布订阅模式的核心组成部分

  1. Publisher(发布者):发布消息到调度中心
  2. Subscriber(订阅者):从调度中心订阅消息
  3. 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发布

#实际应用场景

  1. 前端框架中的 事件总线:Vue 中的 EventBus,用于组件间的通信
  2. 消息队列系统:RabbitMQ、Kafka 等消息中间件
  3. 浏览器的 自定义事件:使用 CustomEvent API 进行复杂事件通信
  4. Redux/Vuex 等状态管理库:通过 action/mutation 发布状态变更

#观察者模式与发布订阅模式的区别

虽然这两种模式经常被混淆,但它们有几个关键区别:

  1. 组件关系

    • 观察者模式:观察者与被观察者直接交互,存在 直接的依赖关系
    • 发布订阅模式:发布者和订阅者完全解耦,通过中间的 事件通道 通信
  2. 消息传递方式

    • 观察者模式:被观察者 直接调用 观察者的更新方法进行通知
    • 发布订阅模式:发布者不知道消息会被谁接收,只负责发布到特定 主题
  3. 关注点

    • 观察者模式:观察者关注特定的 被观察者对象
    • 发布订阅模式:订阅者关注特定的 事件/主题/频道
  4. 灵活性

    • 观察者模式:耦合度相对较高,扩展性较差
    • 发布订阅模式:完全解耦,扩展性更好,可以实现更复杂的消息路由

下图直观地展示了两种模式的区别:

观察者模式: 发布订阅模式:
+-----------+ +-----------+
| Subject | | Publisher |
+-----------+ +-----------+
| |
| 通知 | 发布
▼ ▼
+-----------+ +-----------+
| Observer | ◄-X-X-X-X-X-X-> | EventBus |
+-----------+ +-----------+
|
| 通知
+-----------+
|Subscriber |
+-----------+

#总结

观察者模式和发布订阅模式都是处理对象间通信的有效设计模式,但它们适用于不同的场景:

  • 观察者模式:当系统中存在明确的 一对多依赖关系,且对象间的通信比较简单直接时,应考虑使用。
  • 发布订阅模式:当系统需要 高度解耦,或需要处理复杂的消息路由、过滤和分发时,应考虑使用。

选择哪种模式取决于系统的需求,特别是在考虑 耦合度扩展性性能 之间的权衡时。在实际开发中,可以根据具体情况选择合适的模式,甚至可以结合两种模式的优点来设计更灵活的事件系统。