深度解析:发布订阅模式与观察者模式的核心逻辑与实践

5 阅读10分钟

在软件开发中,“解耦”是永恒的追求之一。发布订阅模式与观察者模式作为两种经典的行为型设计模式,核心价值就在于解耦依赖关系——让数据变化的“通知者”与“接收者”互不直接依赖,从而提升代码的灵活性和可维护性。它们广泛应用于框架设计(如Vue的响应式、Node.js的事件模块)、异步编程等场景。但很多开发者容易混淆两者,本文将从定义、实现、差异、应用四个维度,彻底讲透这两种模式。

一、先搞懂基础:观察者模式

1. 模式定义

观察者模式(Observer Pattern)定义了对象间的一对多依赖关系:当一个对象(被观察者/Subject)的状态发生变化时,所有依赖它的对象(观察者/Observer)都会自动收到通知并进行更新。

通俗理解:就像老师(被观察者)讲课,学生(观察者)认真听讲。当老师讲完一个知识点(状态变化),所有听课的学生(观察者)都会同步接收这个“知识点通知”并记录笔记(更新操作)。老师和学生之间是直接关联的——老师知道哪些学生在听课。

2. 核心角色

观察者模式包含两个核心角色,职责清晰区分:

  • 被观察者(Subject) :维护一个观察者列表,提供三个核心方法——注册观察者(addObserver)移除观察者(removeObserver)通知所有观察者(notifyObservers)。被观察者状态变化时,通过notifyObservers主动触发观察者的更新。
  • 观察者(Observer) :定义一个统一的更新方法(如update),用于接收被观察者的通知并执行具体的业务逻辑。所有观察者必须实现该接口,保证被观察者通知时的调用一致性。

3. 手写实现简易观察者模式

基于核心角色,我们用JavaScript实现一个观察者模式示例(以“老师讲课”场景为例):

// 1. 被观察者:老师(Subject)
class Teacher {
  constructor() {
    this.observers = []; // 维护观察者列表(学生)
    this.knowledge = ''; // 被观察者状态:当前讲解的知识点
  }

  // 注册观察者(学生报名听课)
  addObserver(observer) {
    // 避免重复注册
    if (!this.observers.includes(observer)) {
      this.observers.push(observer);
    }
  }

  // 移除观察者(学生退课)
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  // 通知所有观察者(讲完知识点,通知学生记录)
  notifyObservers() {
    this.observers.forEach(observer => {
      observer.update(this.knowledge); // 传递状态给观察者
    });
  }

  // 状态变化:讲解新知识点
  teach(knowledge) {
    this.knowledge = knowledge; // 更新状态
    this.notifyObservers(); // 状态变化后主动通知观察者
  }
}

// 2. 观察者:学生(Observer)- 实现统一的update方法
class Student {
  constructor(name) {
    this.name = name; // 学生姓名(区分不同观察者)
  }

  // 统一的更新方法:接收通知并执行操作
  update(knowledge) {
    console.log(`${this.name} 收到知识点:${knowledge},已记录笔记`);
  }
}

// 3. 测试代码
const teacher = new Teacher();
const student1 = new Student('小明');
const student2 = new Student('小红');

// 学生报名听课(注册观察者)
teacher.addObserver(student1);
teacher.addObserver(student2);

// 老师讲解知识点(状态变化,触发通知)
teacher.teach('JavaScript 闭包'); 
// 输出:小明 收到知识点:JavaScript 闭包,已记录笔记
// 输出:小红 收到知识点:JavaScript 闭包,已记录笔记

// 小明退课(移除观察者)
teacher.removeObserver(student1);

// 老师讲解新知识点
teacher.teach('Vue 响应式原理');
// 输出:小红 收到知识点:Vue 响应式原理,已记录笔记

实现要点:被观察者主动维护观察者列表,状态变化时直接遍历列表通知观察者,观察者与被观察者是直接关联的。

二、进阶:发布订阅模式

1. 模式定义

发布订阅模式(Publish-Subscribe Pattern)是观察者模式的升级版,它引入了一个“中间件”——事件中心(Event Bus/Event Center) ,用于解耦发布者和订阅者。

核心逻辑:发布者(Publisher)不直接通知订阅者(Subscriber),而是向事件中心发布一个“事件”;订阅者不直接依赖发布者,而是向事件中心订阅感兴趣的“事件”;当事件中心收到发布者的事件后,再通知所有订阅该事件的订阅者。

通俗理解:就像公众号(发布者)发文,读者(订阅者)订阅公众号。公众号发文时(发布事件),不会直接通知每个读者,而是将文章交给微信平台(事件中心);微信平台再将文章推送给所有订阅该公众号的读者(通知订阅者)。公众号不知道有哪些读者订阅,读者也不知道文章是谁写的——两者通过微信平台完全解耦。

2. 核心角色

相比观察者模式,发布订阅模式多了“事件中心”角色,三者职责如下:

  • 发布者(Publisher) :无需维护订阅者列表,仅负责向事件中心发布事件(包含事件名称和事件数据)。
  • 订阅者(Subscriber) :无需依赖发布者,仅负责向事件中心订阅感兴趣的事件,并注册一个回调函数;当事件被触发时,事件中心会执行该回调函数。
  • 事件中心(Event Center) :核心中间件,维护“事件名称-订阅者回调列表”的映射关系,提供三个核心方法——订阅事件(on)取消订阅(off)发布事件(emit)

3. 手写实现简易发布订阅模式

以“公众号发文”场景为例,用JavaScript实现发布订阅模式:

// 1. 事件中心(Event Center)- 核心中间件
class EventCenter {
  constructor() {
    this.eventMap = {}; // 维护事件映射:{ 事件名称: [回调1, 回调2, ...] }
  }

  // 订阅事件:参数(事件名称,回调函数)
  on(eventName, callback) {
    if (!this.eventMap[eventName]) {
      this.eventMap[eventName] = []; // 初始化该事件的回调列表
    }
    // 避免重复订阅同一个回调
    if (!this.eventMap[eventName].includes(callback)) {
      this.eventMap[eventName].push(callback);
    }
  }

  // 取消订阅:参数(事件名称,回调函数)
  off(eventName, callback) {
    const callbacks = this.eventMap[eventName];
    if (!callbacks) return;
    // 过滤掉要取消的回调
    this.eventMap[eventName] = callbacks.filter(cb => cb !== callback);
    // 若事件无回调,可删除该事件(优化内存)
    if (this.eventMap[eventName].length === 0) {
      delete this.eventMap[eventName];
    }
  }

  // 发布事件:参数(事件名称,事件数据)
  emit(eventName, data) {
    const callbacks = this.eventMap[eventName];
    if (!callbacks) return; // 无订阅者,直接返回
    // 执行所有订阅该事件的回调,传递事件数据
    callbacks.forEach(callback => callback(data));
  }
}

// 2. 发布者:公众号
class PublicAccount {
  constructor(name, eventCenter) {
    this.name = name; // 公众号名称
    this.eventCenter = eventCenter; // 依赖事件中心
  }

  // 发布文章(发布事件)
  publishArticle(title, content) {
    const article = { title, content, author: this.name };
    // 向事件中心发布“article-publish”事件,传递文章数据
    this.eventCenter.emit('article-publish', article);
  }
}

// 3. 订阅者:读者
class Reader {
  constructor(name) {
    this.name = name;
  }

  // 订阅公众号文章的回调函数
  receiveArticle(article) {
    console.log(`${this.name} 收到 ${article.author} 的文章:《${article.title}》,内容:${article.content}`);
  }
}

// 4. 测试代码
// 初始化事件中心
const eventCenter = new EventCenter();

// 初始化发布者(公众号)
const vueAccount = new PublicAccount('Vue官方公众号', eventCenter);
const reactAccount = new PublicAccount('React官方公众号', eventCenter);

// 初始化订阅者(读者)
const reader1 = new Reader('小明');
const reader2 = new Reader('小红');

// 读者订阅“article-publish”事件(关联回调函数)
eventCenter.on('article-publish', reader1.receiveArticle.bind(reader1));
eventCenter.on('article-publish', reader2.receiveArticle.bind(reader2));

// Vue公众号发布文章(发布事件)
vueAccount.publishArticle('Vue3 组合式API实战', '本文讲解组合式API的核心用法...');
// 输出:小明 收到 Vue官方公众号 的文章:《Vue3 组合式API实战》,内容:本文讲解组合式API的核心用法...
// 输出:小红 收到 Vue官方公众号 的文章:《Vue3 组合式API实战》,内容:本文讲解组合式API的核心用法...

// 小红取消订阅
eventCenter.off('article-publish', reader2.receiveArticle.bind(reader2));

// React公众号发布文章
reactAccount.publishArticle('React 18 并发渲染', '并发渲染是React 18的核心特性...');
// 输出:小明 收到 React官方公众号 的文章:《React 18 并发渲染》,内容:并发渲染是React 18的核心特性...

实现要点:发布者和订阅者完全不直接关联,所有交互都通过事件中心完成。事件中心是“调度核心”,负责管理事件和回调的映射关系。

三、关键对比:观察者模式 vs 发布订阅模式

很多开发者会把两者等同,但实际上核心差异在于是否存在中间件(事件中心) ,这也导致了两者在“耦合度”和“灵活性”上的差异。下面通过表格清晰对比:

对比维度观察者模式发布订阅模式
核心关联被观察者与观察者直接关联(被观察者知道所有观察者)发布者与订阅者完全解耦(通过事件中心间接关联)
角色数量2个核心角色(被观察者、观察者)3个核心角色(发布者、订阅者、事件中心)
通信方式被观察者主动通知观察者(直接调用观察者的update方法)发布者发布事件到事件中心,事件中心转发给订阅者
耦合度较高(观察者必须依赖被观察者的接口,被观察者需维护观察者列表)较低(发布者和订阅者无需知道对方存在,只需依赖事件中心)
灵活性较低(仅支持“被观察者变化→所有观察者更新”的固定逻辑)较高(支持多事件、跨模块通信,可灵活扩展事件类型)
适用场景简单的一对一/一对多依赖关系(如组件内部的状态通知)复杂的跨模块、多组件通信(如全局状态管理、框架事件系统)

一句话总结:发布订阅模式是观察者模式的“解耦升级版”,核心差异在于是否通过事件中心隔离发布者/订阅者(或被观察者/观察者)。

四、实际应用场景:两种模式在哪里用?

1. 观察者模式的应用

观察者模式适用于“局部、简单的依赖通知”场景,典型案例:

  • Vue2的响应式系统:Vue2中,每个响应式对象(被观察者)维护一个依赖列表(Watcher观察者),当对象属性变化时,直接通知所有Watcher更新视图。这里Watcher与响应式对象是直接关联的,属于观察者模式。
  • DOM事件监听(原生层面) :如element.addEventListener('click', callback),element是被观察者,callback是观察者,点击事件(状态变化)触发时,element直接执行callback,属于观察者模式的简化实现。

2. 发布订阅模式的应用

发布订阅模式适用于“全局、复杂的跨模块通信”场景,典型案例:

  • 前端全局事件总线(Event Bus) :如Vue的this.$bus、React的自定义事件中心,用于跨组件(无父子关系)通信,组件作为发布者/订阅者,通过事件中心传递数据。
  • Node.js的事件模块:Node.js的events.EventEmitter是典型的发布订阅实现,如on(订阅)、emit(发布)、off(取消订阅),广泛用于流、网络请求等模块的事件通知。
  • 状态管理库:如Redux、Vuex,内部通过发布订阅模式实现“状态变化→组件更新”——状态变更(发布者)发布事件,订阅了状态的组件(订阅者)收到通知后重新渲染。
  • 消息队列(MQ) :如RabbitMQ、Kafka,本质是分布式的发布订阅系统——生产者(发布者)发布消息到队列(事件中心),消费者(订阅者)从队列订阅消息,实现分布式系统的解耦通信。

五、总结与实践建议

两种模式的核心价值都是“解耦依赖、统一通知”,但适用场景各有侧重,实践中可遵循以下建议:

  1. 简单场景用观察者模式:如果是模块内部的局部依赖通知(如一个对象的状态变化需要通知几个相关对象),直接用观察者模式即可,避免引入事件中心增加复杂度。
  2. 复杂场景用发布订阅模式:如果是跨模块、跨组件、甚至跨系统的通信(如全局状态管理、分布式系统),优先用发布订阅模式,通过事件中心彻底解耦各方,提升系统的可扩展性。
  3. 避免过度设计:不要为了“用模式”而用模式。如果只是简单的函数调用就能解决问题(如父子组件传值),无需强行引入观察者/发布订阅模式。

最后,记住:发布订阅模式不是否定观察者模式,而是在其基础上通过“中间件”优化了耦合度。理解两者的核心差异,才能在合适的场景选择合适的模式,写出更优雅、可维护的代码。