11、MVC中的观察者模式:模型如何通知视图更新
观察者模式,说白了就是「发布-订阅」那一套。在MVC里,它解决了一个很实际的问题:模型数据变了,视图怎么知道?
我刚开始用MVC的时候,也犯过傻——直接在模型里写死视图的更新方法。结果呢?换一个视图就得改模型代码,维护起来简直想骂人。后来我才明白,观察者模式就是专门干这个的。
什么是观察者模式
先看个简单的定义:
观察者模式定义了对象之间的一对多依赖关系。当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。
在MVC里,这个「一」就是模型(Model),「多」就是视图(View)。模型变了,所有注册过的视图都得跟着变。
举个例子你就懂了:
- 模型:用户数据(比如姓名、年龄)
- 视图:用户信息展示面板、日志输出面板、统计图表
用户改了名字,模型一更新,三个视图全得刷新。总不能每个视图都去轮询吧?
观察者模式的核心角色
我习惯把观察者模式拆成四个角色:
| 角色 | MVC中的对应 | 职责 |
|---|---|---|
| Subject(主题) | Model | 维护观察者列表,提供注册/移除方法 |
| Observer(观察者) | View | 定义更新接口,收到通知后执行刷新 |
| ConcreteSubject(具体主题) | 具体的Model类 | 存储真实数据,状态变化时通知所有观察者 |
| ConcreteObserver(具体观察者) | 具体的View类 | 实现更新逻辑,比如重新渲染界面 |
嗯,这里要注意:观察者模式不是MVC独有的。事件驱动、消息队列、甚至前端框架的响应式系统,底层都有它的影子。
代码示例:一个简单的观察者模式实现
我用JavaScript写个最简版本,方便你理解:
// 1. 主题(Subject)—— 也就是模型基类
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// 2. 具体主题(ConcreteSubject)—— 用户模型
class UserModel extends Subject {
constructor(name, age) {
super();
this.name = name;
this.age = age;
}
setName(newName) {
this.name = newName;
this.notifyObservers({ field: 'name', value: newName });
}
setAge(newAge) {
this.age = newAge;
this.notifyObservers({ field: 'age', value: newAge });
}
}
// 3. 观察者接口(Observer)
class Observer {
update(data) {
// 子类实现
}
}
// 4. 具体观察者(ConcreteObserver)—— 用户信息视图
class UserInfoView extends Observer {
update(data) {
console.log(`用户信息视图更新:${data.field} 变为 ${data.value}`);
// 实际项目中这里会操作DOM
}
}
// 5. 另一个观察者 —— 日志视图
class LogView extends Observer {
update(data) {
console.log(`日志记录:字段 ${data.field} 更新为 ${data.value}`);
}
}
// 使用示例
const userModel = new UserModel('张三', 25);
const infoView = new UserInfoView();
const logView = new LogView();
userModel.addObserver(infoView);
userModel.addObserver(logView);
userModel.setName('李四');
// 输出:
// 用户信息视图更新:name 变为 李四
// 日志记录:字段 name 更新为 李四
你看,模型只管通知,视图只管更新。两者之间没有硬编码的依赖关系。这就是解耦。
观察者模式在MVC中的完整流程
我画了一张图,帮你理清整个流程:
流程其实就四步:
- 用户操作视图,触发事件
- 控制器接收事件,调用模型的方法
- 模型更新数据,然后遍历观察者列表
- 每个观察者(视图)收到通知,执行自己的更新逻辑
关键点在哪?模型不知道视图是谁,视图也不知道模型内部怎么实现的。它们只通过一个update接口通信。
我在项目中踩过的坑
讲几个真实教训:
⚠️ 坑一:观察者忘记移除
我曾经在一个长期运行的后台服务里,每次创建新视图都往模型上注册观察者,但视图销毁时忘了移除。结果呢?模型一更新,一堆已经不存在的视图还在被通知,内存泄漏不说,还报了一堆空指针异常。
解决方案:在视图的销毁方法里,一定要调用 removeObserver。或者用弱引用(WeakReference)来持有观察者。
⚠️ 坑二:通知顺序导致的问题
有一次我同时注册了两个视图,视图A更新时会触发视图B的更新,视图B更新又会反过来影响视图A。结果陷入了死循环,CPU直接飙到100%。
解决方案:加一个更新锁,或者在通知时传递一个「是否正在更新」的标志位。更优雅的做法是用事件队列,异步处理通知。
💡 小技巧:批量更新
如果模型一次改了多个字段,不要每个字段都通知一次。我习惯的做法是:先修改所有字段,最后统一调用一次 notifyObservers。这样视图只刷新一次,性能好很多。
观察者模式的变体:事件驱动
在实际项目中,我很少直接手写观察者模式。更多是用事件机制:
// 用事件代替观察者
class EventBus {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
const callbacks = this.events[eventName] || [];
callbacks.forEach(cb => cb(data));
}
off(eventName, callback) {
const callbacks = this.events[eventName] || [];
this.events[eventName] = callbacks.filter(cb => cb !== callback);
}
}
// 模型使用事件总线
class UserModel {
constructor(eventBus) {
this.eventBus = eventBus;
this.name = '';
}
setName(name) {
this.name = name;
this.eventBus.emit('user:nameChanged', { name });
}
}
// 视图监听事件
const eventBus = new EventBus();
const userModel = new UserModel(eventBus);
eventBus.on('user:nameChanged', (data) => {
console.log('视图A更新:', data.name);
});
eventBus.on('user:nameChanged', (data) => {
console.log('视图B更新:', data.name);
});
事件总线的优势很明显:解耦更彻底。模型不需要维护观察者列表,视图也不需要实现特定的接口。大家只跟事件总线打交道。
什么时候该用观察者模式
我总结了几条经验:
- 一个对象变化,多个对象要跟着变 —— 比如用户信息改了,多个面板都要刷新
- 你不确定会有多少个对象依赖你 —— 观察者模式支持动态增删,扩展性好
- 你想避免硬编码的依赖关系 —— 模型和视图之间只通过接口通信
但也不是万能的。如果视图数量特别多(比如上千个),遍历通知会有性能问题。这时候可以考虑用「脏检查」或者「虚拟DOM diff」来优化。
📌 核心要点回顾
- 观察者模式让模型和视图解耦,模型只负责通知,视图只负责更新
- 四个角色:Subject、Observer、ConcreteSubject、ConcreteObserver
- 注意内存泄漏:视图销毁时一定要移除观察者
- 批量更新比逐个通知更高效
- 事件总线是观察者模式的升级版,解耦更彻底
观察者模式看起来简单,但用好了能让你的MVC架构灵活很多。我见过不少项目,一开始图省事直接在模型里调视图方法,后来改需求改到崩溃。说白了,解耦这件事,越早做越划算。