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中的完整流程

我画了一张图,帮你理清整个流程:

观察者模式在MVC中的完整流程 用户操作 点击/输入/提交 控制器 (Controller) 处理用户输入 模型 (Model) 更新数据 + 通知 观察者列表 视图A (View) 用户信息面板 视图B (View) 日志输出面板 视图C (View) 统计图表 ① 触发操作 ② 更新数据 ③ 遍历通知 ④ 调用update() ① 用户操作 → ② 控制器更新模型 → ③ 模型通知观察者 → ④ 各视图自行刷新

流程其实就四步:

  1. 用户操作视图,触发事件
  2. 控制器接收事件,调用模型的方法
  3. 模型更新数据,然后遍历观察者列表
  4. 每个观察者(视图)收到通知,执行自己的更新逻辑

关键点在哪?模型不知道视图是谁,视图也不知道模型内部怎么实现的。它们只通过一个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架构灵活很多。我见过不少项目,一开始图省事直接在模型里调视图方法,后来改需求改到崩溃。说白了,解耦这件事,越早做越划算


公众号:蓝海资料掘金营,微信deep3321