Skip to content

2.3 观察者模式 (Observer Pattern)

欢迎来到观察者模式的学习!这是行为型模式中的“明星”,也是理解现代前端“响应式”原理的钥匙。

核心定义:定义了对象之间一种一对多的依赖关系。当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者们)都将得到通知并自动更新。

一、生活中的场景:UP 主与粉丝

这个模式在生活中最贴切的例子就是你在 B 站关注 UP 主:

  • UP 主 (Subject / 被观察者):他是内容的发布者,是“被观察”的对象。
  • 你和其他粉丝 (Observers / 观察者):你们都订阅(subscribe)了 UP 主。
  • UP 主更新视频 (State Change):当 UP 主发布新视频时,他会通过平台通知 (notify) 所有关注他的粉丝。
  • 你收到更新提醒 (Update):你和其他粉丝的动态中都出现了新视频,你们的“状态”被自动更新了。

在这个关系中,UP 主不需要知道每个粉丝的名字和联系方式。他只管向所有“订阅者”这个群体发送通知。你也可以随时“取关” (unsubscribe),不再接收通知。这是一种非常松散、灵活的耦合关系

二、没有观察者模式的代码:紧密的耦合

假设我们有一个核心数据对象 appState,当它的数据变化时,我们需要更新页面上的两个模块:一个头部购物车计数器 header 和一个商品预览模块 preview

javascript
// 模块一:头部
const header = {
  update: function (data) {
    console.log(`头部更新:购物车中有 ${data.cartCount} 件商品。`);
  },
};

// 模块二:预览
const preview = {
  update: function (data) {
    console.log(`预览模块更新:当前选中商品 ID 为 ${data.selectedId}。`);
  },
};

// 核心数据对象
const appState = {
  cartCount: 0,
  selectedId: null,
  // 每次数据变化时,我们必须手动调用所有依赖它的模块!
  setData: function (data) {
    this.cartCount = data.cartCount || this.cartCount;
    this.selectedId = data.selectedId || this.selectedId;

    // 硬编码通知,紧密耦合
    header.update(this);
    preview.update(this);
  },
};

appState.setData({ cartCount: 5 });
// 输出:
// 头部更新:购物车中有 5 件商品。
// 预览模块更新:当前选中商品 ID 为 null。

appState.setData({ selectedId: 101 });
// 输出:
// 头部更新:购物车中有 5 件商品。
// 预览模块更新:当前选中商品 ID 为 101。

这段代码的问题非常明显:

  1. 高耦合appState 必须明确知道 headerpreview 这两个对象存在,并负责调用它们的 update 方法。它们之间是“硬编码”的强依赖关系。
  2. 违反开放/封闭原则:如果未来新增一个 footer 模块也需要监听 appState 的变化,我们必须修改 appStatesetData 方法,在里面加上 footer.update(this)

三、使用观察者模式重构

我们的目标是:让 appState 从一个“主动的通知者”变成一个“被动的 UP 主”,它只管发布更新,而不需要关心谁订阅了它。

第 1 步:创建“被观察者” (Subject) 类

这个类管理着一个“粉丝列表”(observers),并提供“订阅”、“取关”和“通知”的功能。

javascript
class Subject {
  constructor() {
    this.observers = []; // 粉丝列表
  }

  // 添加观察者(订阅)
  subscribe(observer) {
    this.observers.push(observer);
  }

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

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

第 2 步:创建“观察者” (Observer)

观察者很简单,它只需要提供一个 update 方法,供 Subject 调用。我们之前的 headerpreview 模块已经符合这个约定了。

javascript
// header 和 preview 对象本身就是合格的观察者,因为它们都有 update 方法
const header = {
  /* ... */
};
const preview = {
  /* ... */
};

第 3 步:重构 appState 并建立关系

appState 成为一个 Subject,并在数据变化时调用 notify

javascript
// 让 appState 继承 Subject 的能力
const appState = new Subject();
appState.data = {
  cartCount: 0,
  selectedId: null,
};

// 提供一个修改数据的方法
appState.setData = function (data) {
  Object.assign(this.data, data);
  // 当数据变化时,通知所有“粉丝”
  this.notify(this.data);
};

// =======================================================
// 建立关系:让 header 和 preview 订阅 appState
appState.subscribe(header);
appState.subscribe(preview);

// =======================================================
// 现在,我们只需要修改数据,更新会自动发生!
appState.setData({ cartCount: 10 });
// 输出:
// 头部更新:购物车中有 10 件商品。
// 预览模块更新:当前选中商品 ID 为 null。

// 新增一个观察者,完全不需要修改 appState 的代码
const footer = {
  update: function (data) {
    console.log(`页脚更新:购物车数量 ${data.cartCount}`);
  },
};
appState.subscribe(footer);

appState.setData({ selectedId: 205 });
// 输出:
// 头部更新:购物车中有 10 件商品。
// 预览模块更新:当前选中商品 ID 为 205。
// 页脚更新:购物车数量 10

现在,appState 和各个 UI 模块彻底解耦了。appState 只负责维护数据和发布通知,UI 模块只负责订阅和更新,各司其职。

四、与发布/订阅模式 (Pub/Sub) 的区别

你可能听说过一个和观察者模式非常相似的模式:发布/订阅模式。

  • 观察者模式:Subject 直接持有 Observers 的引用并调用它们的方法。它们之间是直接的依赖关系。(UP 主直接@粉丝)
  • 发布/订阅模式:发布者 (Publisher) 和订阅者 (Subscriber) 互相不知道对方的存在。它们通过一个第三方的事件中心 (Event Bus / Broker) 来通信。发布者向中心发布事件,订阅者向中心订阅事件。(作者在报社投稿,读者去报亭买报纸,作者和读者互不相识,报社是中介)

发布/订阅模式的耦合度更低,但引入了额外的复杂性。在很多场景下,它们可以解决相似的问题。

五、优缺点与适用场景

优点

  1. 低耦合:完美地解耦了被观察者和观察者。
  2. 符合开闭原则:可以随时增加新的观察者,而无需修改被观察者的代码。
  3. 广播机制:一次状态变更可以通知到所有相关的对象。

缺点

  1. 内存泄漏风险:如果一个观察者被销毁了,但没有从被观察者的列表中 unsubscribe,会导致内存泄漏(“僵尸监听器”)。
  2. 调试困难:由于是松耦合,调用链是不明确的,当一个更新逻辑出错时,可能难以追踪是谁发出的通知。
  3. 更新顺序问题:默认情况下,观察者接收通知的顺序是不确定的。

适用场景

  • 当一个对象的状态变化需要改变其他对象,但你不想让这些对象之间形成紧密耦合时。
  • UI 开发中的事件监听和数据绑定(DOM 事件、Vue/React 的状态管理)。
  • 实现消息队列或事件总线系统。