【JavaScript】观察者模式 vs 发布订阅模式:解耦通信的“微信群”与“电话群”

105 阅读5分钟

一、观察者模式:从“电话”到设计

1. 观察者模式的定义与核心结构

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

是不是感觉和上一篇中的发布订阅模式很像?你先别急,慢慢往下看


2. 气象台的电话通知

假设你住在山区,每天通过电话向气象台“订阅”天气预报。气象台每天固定时间打电话通知你:“明天有暴雨,请注意安全!”

  • 气象台(被观察者):维护所有订阅电话的列表,当天气变化时主动拨打电话。
  • (观察者):收到电话后执行具体操作(如准备雨具、转移物资)。

这种 “直接通知” 的机制,是观察者模式的核心思想。

核心角色

  1. 被观察者(Subject)

    • 维护观察者列表,提供添加/移除观察者的方法。
    • 在状态变化时主动调用观察者的更新方法。
  2. 观察者(Observer)

    • 定义统一的更新接口(如 update() 方法)。
    • 当收到通知时,执行具体操作。

3. 观察者模式的代码实现

以“气象台通知用户”为例:

// 被观察者:气象台
class WeatherStation {
  constructor() {
    this.observers = []; // 保存所有订阅者
    this.temperature = 0; // 当前温度
  }

  // 添加观察者(用户)
  addObserver(observer) {
    this.observers.push(observer);
  }

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

  // 设置温度并通知所有观察者
  setTemperature(temp) {
    this.temperature = temp;
    this.notifyObservers();
  }

  // 通知所有观察者
  notifyObservers() {
    this.observers.forEach(observer => {
      observer.update(this.temperature);
    });
  }
}

// 观察者:用户
class User {
  constructor(name) {
    this.name = name;
  }

  update(temp) {
    console.log(`${this.name} 收到通知:当前温度为 ${temp}℃`);
  }
}

// 使用示例
const station = new WeatherStation();
const userA = new User("张三");
const userB = new User("李四");

station.addObserver(userA);
station.addObserver(userB);

station.setTemperature(30); // 气象台更新温度,通知所有用户

输出结果

张三 收到通知:当前温度为 30℃
李四 收到通知:当前温度为 30

二、发布订阅模式:从“群聊”到设计

1. 生活场景引入:微信群聊通知

你加入了一个“暴雨预警”微信群,群里有气象局、应急办等多个成员。当气象局发布“暴雨预警”消息时,群里的所有成员(包括你)都会收到通知。

  • 微信群(事件中心):管理消息的发布与订阅。
  • 气象局(发布者):只负责发布消息,不关心谁会收到。
  • (订阅者):只接收感兴趣的消息,不关心消息来源。

这种 “间接通知” 的机制,是发布订阅模式的核心思想。


2. 发布订阅模式的代码实现

通过事件中心(Event Bus)解耦发布者和订阅者:

class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
  }

  emit(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(data));
    }
  }
}

三、观察者模式 vs 发布订阅模式:全方位对比

维度观察者模式发布订阅模式
通信方式被观察者直接调用观察者的 update() 方法,通信是同步的。事件中心异步分发消息,订阅者通过回调函数接收消息。
耦合度高耦合:被观察者直接持有观察者的引用,两者需要知道对方的存在。低耦合:发布者和订阅者通过事件中心通信,彼此无需知道对方的具体实现。
依赖关系被观察者和观察者之间存在直接依赖关系。发布者和订阅者通过事件中心间接通信,无直接依赖。
灵活性被观察者需要维护观察者列表,扩展性较低。事件中心独立于发布者和订阅者,扩展性强,可动态添加/移除订阅者。
适用场景适用于强耦合的场景,如 UI 更新、响应式数据绑定(Vue/React)。适用于松耦合的场景,如微服务通信、消息队列(Kafka/RabbitMQ)、跨层级组件通信。
典型应用- Vue 的响应式系统(Vue 2)
- DOM 事件模型(如 addEventListener
- Redux/Vuex 状态管理
- Node.js 的 EventEmitter
- 微服务架构中的事件驱动

四、Vue 响应式:观察者模式的典型应用(拓展)

1. Vue 2 的响应式原理

Vue 2 通过 Object.defineProperty 实现响应式数据:

  • 数据劫持:遍历 data 对象的所有属性,设置 getset 拦截读写。
  • 依赖收集:在 get 中收集 Watcher(观察者),建立数据与视图的依赖关系。
  • 派发更新:在 set 中触发依赖更新,通知 Watcher 重新渲染视图。

1.1 什么是 Object.defineProperty

Object.defineProperty 是 JavaScript 原生方法,用于直接在对象上定义新属性或修改现有属性,并返回该对象。通过设置 getset 方法,可以拦截属性的读写操作,实现数据劫持。

语法
Object.defineProperty(obj, prop, descriptor);
  • 参数

    • obj:要在其上定义属性的对象。
    • prop:要定义或修改的属性的名称。
    • descriptor:属性描述符,包含 valuewritableenumerableconfigurablegetset 等特性。
关键特性
  • 数据描述符:定义属性的值(value)和可写性(writable)。
  • 存取描述符:通过 get 和 set 拦截属性的读写操作。

1.2 Object.defineProperty 的代码实现

const obj = {};
let internalValue = 0;

Object.defineProperty(obj, 'count', {
  get() {
    console.log('读取 count 属性');
    return internalValue;
  },
  set(newVal) {
    console.log('修改 count 属性为', newVal);
    internalValue = newVal;
  },
  enumerable: true,
  configurable: true
});

obj.count = 10; // 修改 count 属性为 10
console.log(obj.count); // 读取 count 属性 → 输出 10

劫持属性后使用get或set方法进行数据更新和通知发布的的操作即可实现响应式更新视图

本文此处知识点目前作为拓展介绍,具体源码分析不是本文侧重点。故不附源码实现

2. 局限性

  • 无法监听数组索引变化(需重写数组方法)。
  • 无法检测属性的添加/删除(需手动调用 Vue.set)。
  • 需递归遍历对象,性能较差。

五、总结

  • 观察者模式:像电话群,直接通知,适合强耦合场景(如 Vue 响应式)。
  • 发布订阅模式:像微信群,间接通信,适合松耦合场景(如微服务通信)。

通过合理选择这两种模式,可以显著提升代码的可维护性和扩展性。理解它们的区别,能让你在设计系统时灵活高效! 🌟