观察者模式渐进式学习指南
写给前端开发者的一篇入门笔记:先把观察者模式最核心的关系想明白,再看它为什么经常和发布订阅模式混在一起,以及项目里什么时候该用它。
目录
- 什么是观察者模式?
- 它到底在解决什么问题?
- 先从一个最小例子看懂它
- 观察者模式里到底有谁
- 它和发布订阅模式有什么区别
- 前端里常见的观察者思路
- 手写一个最小观察者模式
- 再往前走一步:支持移除观察者
- 观察者模式的优点和问题
- 几个很容易踩的坑
- 如果放到面试里,怎么讲比较自然
- 练习题
- 最后做个收束
一、什么是观察者模式?
观察者模式(Observer Pattern)本质上是在描述一种一对多依赖关系。
简单说就是:
一个对象的状态发生变化时,依赖它的其他对象会收到通知,并做出相应更新。
这个模式里最重要的不是“通知”这件事本身,而是“谁依赖谁”这层关系。
通常我们会把它拆成两个角色:
- 目标对象 / 被观察者(Subject):状态发生变化的一方
- 观察者(Observer):依赖目标对象,并在变化发生时更新自己的一方
如果说得再白一点,观察者模式更像这样一种关系:
我一直盯着你,你一变,我就跟着变。
这就是它和很多“消息转发式”模式最大的不同:观察者和目标对象之间,通常是直接建立联系的。
二、它到底在解决什么问题?
前端里很常见的一类问题是:一个状态变了,别的地方也要跟着变。
比如:
- 用户信息更新后,头部头像、昵称、个人中心都要刷新
- 主题切换后,多个组件都要重新应用主题色
- 某个数据源变化后,依赖它的视图要重渲染
- 表单值变化后,校验状态和按钮禁用状态都要重新计算
如果这些联动全都靠手动调用,很快就会写成这种样子:
function setTheme(theme) {
updateHeader(theme)
updateSidebar(theme)
updateContent(theme)
}
短期看似乎没毛病,长期问题不少:
- 谁该更新,要靠你手动维护
- 新增联动逻辑时,原函数还得继续改
- 一旦依赖关系变复杂,代码边界就开始变糊
观察者模式的思路是:
- 目标对象只负责维护自己的状态
- 观察者自己挂到目标对象上
- 状态变化时,由目标对象统一通知这些观察者
这样一来,状态变化和界面联动之间就有了更稳定的关系结构。
三、先从一个最小例子看懂它
先看一个最小版本。
class Subject {
constructor() {
this.observers = []
this.state = '默认状态'
}
addObserver(observer) {
this.observers.push(observer)
}
setState(newState) {
this.state = newState
this.notify()
}
notify() {
this.observers.forEach((observer) => observer.update(this.state))
}
}
class Observer {
constructor(name) {
this.name = name
}
update(state) {
console.log(`${this.name} 收到更新:${state}`)
}
}
const subject = new Subject()
const observer1 = new Observer('导航栏')
const observer2 = new Observer('个人中心')
subject.addObserver(observer1)
subject.addObserver(observer2)
subject.setState('用户已登录')
输出结果:
导航栏 收到更新:用户已登录
个人中心 收到更新:用户已登录
这里发生的事情很简单:
subject保存了观察者列表- 观察者通过
addObserver()加入列表 subject状态变化后,调用notify()- 所有观察者都执行自己的
update()
这个例子虽然很小,但已经把观察者模式最核心的关系说明白了。
四、观察者模式里到底有谁
1. 目标对象(Subject)
目标对象是“被观察”的那一方。
它通常负责三件事:
- 保存观察者列表
- 提供注册和移除观察者的方法
- 状态变化时通知观察者
2. 观察者(Observer)
观察者是“依赖目标对象”的那一方。
它至少要有一个统一的更新接口,比如:
update(newState)
一旦目标对象变化,它就会收到通知,并决定自己怎么处理。
3. 关系重点
观察者模式最关键的点是:
目标对象知道观察者是谁。
也就是说,它通常是直接维护观察者列表的。
这一点后面和发布订阅模式对比时会特别明显。
五、它和发布订阅模式有什么区别
这两个模式几乎是前端学习路上最容易串台的一对。
一个简单结论
观察者模式是目标对象直接通知观察者;发布订阅模式是通过中间层转发消息。
观察者模式更像什么
在观察者模式里:
- 目标对象维护观察者列表
- 观察者直接注册到目标对象上
- 通知由目标对象亲自发出
也就是说,双方之间通常存在直接关系。
发布订阅模式更像什么
在发布订阅模式里:
- 发布者不直接维护订阅者
- 订阅者也不直接挂在发布者身上
- 中间多了一层事件中心或消息中心
所以它们最大的区别,不是“能不能通知”,而是耦合关系在哪一层。
可以这么记
| 模式 | 关系特点 |
|---|---|
| 观察者模式 | 目标对象直接维护观察者 |
| 发布订阅模式 | 发布者和订阅者通过中间层联系 |
为什么这两个模式总被放一起讲
因为它们都在处理“一个变化引发多个响应”这件事,只是组织方式不同。
前端开发里,如果你只是想理解“联动更新”的思想,这两个模式经常会有重叠感;但如果你要讲清楚设计关系,还是得分开。
六、前端里常见的观察者思路
虽然你不一定会手写经典的 Subject 和 Observer 类,但观察者模式的思想在前端里其实很常见。
1. 响应式系统
像 Vue 这类框架里的响应式更新,本质上就很接近观察者思路:
- 某个响应式数据是被观察对象
- 依赖这个数据的副作用、渲染逻辑、计算逻辑会被收集起来
- 数据变化时,再统一触发这些依赖更新
当然,框架实现会复杂得多,但底层味道很像。
2. 表单联动
比如表单值变化后:
- 校验状态变化
- 按钮禁用状态变化
- 错误提示变化
如果这些逻辑都依赖同一个状态源,那本质上就有观察者关系。
3. MVC / MVVM 里的视图更新
模型数据变化后,视图跟着更新,这也是很典型的“被观察对象变化,观察者更新”。
4. 自己写一个轻量数据源
小项目里有时会手写一个简单的数据容器,让多个组件或模块依赖它。只要它维护的是“依赖我变化的一组对象”,那观察者模式的影子就已经在里面了。
七、手写一个最小观察者模式
我们把前面的例子稍微整理得更像一套完整结构。
class Subject {
constructor() {
this.observers = []
this.state = null
}
attach(observer) {
this.observers.push(observer)
}
setState(newState) {
this.state = newState
this.notify()
}
notify() {
this.observers.forEach((observer) => observer.update(this.state))
}
}
class Observer {
constructor(name) {
this.name = name
}
update(state) {
console.log(`${this.name} 检测到状态变化:${state}`)
}
}
const userState = new Subject()
const header = new Observer('Header')
const profile = new Observer('Profile')
userState.attach(header)
userState.attach(profile)
userState.setState('用户已登录')
输出:
Header 检测到状态变化:用户已登录
Profile 检测到状态变化:用户已登录
这里最值得注意的不是代码量,而是关系清晰:
Subject是中心状态源Observer是依赖这个状态源的对象- 状态一变,所有依赖它的对象都同步感知
八、再往前走一步:支持移除观察者
实际使用里,光能注册不够,还得能取消观察。否则组件卸载了、页面切走了,观察者还留在列表里,就很容易出问题。
class Subject {
constructor() {
this.observers = []
this.state = null
}
attach(observer) {
this.observers.push(observer)
}
detach(observer) {
this.observers = this.observers.filter((item) => item !== observer)
}
setState(newState) {
this.state = newState
this.notify()
}
notify() {
this.observers.forEach((observer) => observer.update(this.state))
}
}
class Observer {
constructor(name) {
this.name = name
}
update(state) {
console.log(`${this.name} 收到状态:${state}`)
}
}
这样做的好处很直接:
- 状态源不会无限挂着没用的观察者
- 生命周期更完整
- 后续排查重复触发问题也更容易
这一点在真实项目里比看上去重要得多。
九、观察者模式的优点和问题
它的好处
1. 一对多更新关系很自然 一个目标对象变化,多个观察者自动更新,表达起来非常顺手。
2. 依赖关系比较清晰 谁依赖谁,在结构上是明确的,不需要靠人脑猜。
3. 有利于做响应式和联动更新 很多数据驱动视图的场景,本来就和观察者模式很贴近。
4. 扩展性还不错 新增一个观察者,通常不用大改目标对象本身。
它的问题
1. 目标对象会知道所有观察者 这意味着它和观察者之间不是完全解耦的。
2. 观察者太多时,更新成本会上来 如果挂了一大堆观察者,每次状态变化都要通知所有人,性能和复杂度都可能受影响。
3. 容易出现重复通知或无效通知 如果观察者管理不好,可能会重复注册,最后一个变化触发多次更新。
4. 链式更新时不太好排查 A 变了通知 B,B 又改了别的状态继续通知 C,这种链一长,排查起来就不轻松。
十、几个很容易踩的坑
1. 只注册,不移除
这是最常见的问题。
如果观察者挂上去之后不移除,页面切换、组件卸载后,它还在列表里,就可能出现:
- 重复更新
- 多次执行同一逻辑
- 内存占用增加
2. 观察者里塞太多业务
观察者的 update() 最好聚焦在“收到变化后做自己的事”,如果每个观察者里都塞一大堆额外业务,后面会越来越难拆。
3. 一个状态源承担太多责任
如果什么变化都往一个 Subject 里塞,那它最后会变成一个巨型状态中心。观察者模式就失去了原本那种结构清晰的优势。
4. 把观察者模式和事件总线混着用
这两个不是不能一起出现,但如果你自己脑子里没分清:
- 什么时候是直接依赖关系
- 什么时候是消息中介关系
代码很快就会写成“自己也说不清为什么这么连”的样子。
十一、如果放到面试里,怎么讲比较自然
如果面试官问到观察者模式,讲清楚三件事基本就够了:
- 它是什么
- 它和发布订阅的区别
- 前端里有哪些对应场景
可以概括成下面这段意思:
观察者模式是一种一对多依赖关系的设计模式。目标对象维护观察者列表,当自身状态变化时,会直接通知这些观察者更新。它适合处理数据变化带来的联动更新问题,前端里常见于响应式系统、视图更新、表单联动这类场景。它和发布订阅模式的区别在于,观察者模式里目标对象直接知道观察者,而发布订阅模式通常会多一层事件中心做解耦。
如果被追问“它的缺点是什么”,可以补一句:
它的问题主要在于目标对象和观察者之间仍然有直接关系,观察者太多时会增加管理成本;另外如果注册和移除控制不好,也容易造成重复通知和内存问题。
这样讲已经足够自然,也足够像你真的理解过它,而不是只背过定义。
十二、练习题
练习 1:手写一个最小观察者模式
要求:
- 有一个
Subject - 有多个
Observer Subject状态变化时能通知所有观察者
练习 2:给 Subject 加上移除观察者能力
要求:
- 支持注册观察者
- 支持移除观察者
- 移除后不再收到通知
练习 3:判断下面这些场景更像不像观察者模式
- 响应式数据变化后触发视图更新
- 事件总线广播消息给任意模块
- 表单值变化带动按钮禁用状态更新
- 插件系统通过中间层分发生命周期事件
可以先自己判断,再参考这个方向:
| 场景 | 是否更像观察者模式 | 原因 |
|---|---|---|
| 响应式数据变化后触发视图更新 | 是 | 典型依赖关系更新 |
| 事件总线广播消息给任意模块 | 不太像 | 更接近发布订阅 |
| 表单值变化带动按钮状态更新 | 是 | 状态变化直接驱动依赖项 |
| 插件系统通过中间层分发事件 | 不太像 | 更接近发布订阅 |
十三、最后做个收束
如果只记一句话,那就是:
观察者模式的重点,是让依赖同一个状态源的多个对象,在状态变化时能够自动更新。
它适合这种场景:
- 有一个核心状态源
- 有多个对象依赖这个状态源
- 状态一变,依赖它的对象就该联动更新
理解到这里,再回头看响应式系统、数据驱动视图、表单联动,你会发现观察者模式并没有那么“书本化”,它其实一直都在前端开发里反复出现。