Appearance
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。这段代码的问题非常明显:
- 高耦合:
appState必须明确知道header和preview这两个对象存在,并负责调用它们的update方法。它们之间是“硬编码”的强依赖关系。 - 违反开放/封闭原则:如果未来新增一个
footer模块也需要监听appState的变化,我们必须修改appState的setData方法,在里面加上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 调用。我们之前的 header 和 preview 模块已经符合这个约定了。
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) 来通信。发布者向中心发布事件,订阅者向中心订阅事件。(作者在报社投稿,读者去报亭买报纸,作者和读者互不相识,报社是中介)
发布/订阅模式的耦合度更低,但引入了额外的复杂性。在很多场景下,它们可以解决相似的问题。
五、优缺点与适用场景
优点
- 低耦合:完美地解耦了被观察者和观察者。
- 符合开闭原则:可以随时增加新的观察者,而无需修改被观察者的代码。
- 广播机制:一次状态变更可以通知到所有相关的对象。
缺点
- 内存泄漏风险:如果一个观察者被销毁了,但没有从被观察者的列表中
unsubscribe,会导致内存泄漏(“僵尸监听器”)。 - 调试困难:由于是松耦合,调用链是不明确的,当一个更新逻辑出错时,可能难以追踪是谁发出的通知。
- 更新顺序问题:默认情况下,观察者接收通知的顺序是不确定的。
适用场景
- 当一个对象的状态变化需要改变其他对象,但你不想让这些对象之间形成紧密耦合时。
- UI 开发中的事件监听和数据绑定(DOM 事件、Vue/React 的状态管理)。
- 实现消息队列或事件总线系统。