观察者模式渐进式学习指南

0 阅读12分钟

观察者模式渐进式学习指南

写给前端开发者的一篇入门笔记:先把观察者模式最核心的关系想明白,再看它为什么经常和发布订阅模式混在一起,以及项目里什么时候该用它。


目录

  1. 什么是观察者模式?
  2. 它到底在解决什么问题?
  3. 先从一个最小例子看懂它
  4. 观察者模式里到底有谁
  5. 它和发布订阅模式有什么区别
  6. 前端里常见的观察者思路
  7. 手写一个最小观察者模式
  8. 再往前走一步:支持移除观察者
  9. 观察者模式的优点和问题
  10. 几个很容易踩的坑
  11. 如果放到面试里,怎么讲比较自然
  12. 练习题
  13. 最后做个收束

一、什么是观察者模式?

观察者模式(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. 关系重点

观察者模式最关键的点是:

目标对象知道观察者是谁。

也就是说,它通常是直接维护观察者列表的。

这一点后面和发布订阅模式对比时会特别明显。


五、它和发布订阅模式有什么区别

这两个模式几乎是前端学习路上最容易串台的一对。

一个简单结论

观察者模式是目标对象直接通知观察者;发布订阅模式是通过中间层转发消息。

观察者模式更像什么

在观察者模式里:

  • 目标对象维护观察者列表
  • 观察者直接注册到目标对象上
  • 通知由目标对象亲自发出

也就是说,双方之间通常存在直接关系。

发布订阅模式更像什么

在发布订阅模式里:

  • 发布者不直接维护订阅者
  • 订阅者也不直接挂在发布者身上
  • 中间多了一层事件中心或消息中心

所以它们最大的区别,不是“能不能通知”,而是耦合关系在哪一层

可以这么记

模式关系特点
观察者模式目标对象直接维护观察者
发布订阅模式发布者和订阅者通过中间层联系

为什么这两个模式总被放一起讲

因为它们都在处理“一个变化引发多个响应”这件事,只是组织方式不同。

前端开发里,如果你只是想理解“联动更新”的思想,这两个模式经常会有重叠感;但如果你要讲清楚设计关系,还是得分开。


六、前端里常见的观察者思路

虽然你不一定会手写经典的 SubjectObserver 类,但观察者模式的思想在前端里其实很常见。

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. 它是什么
  2. 它和发布订阅的区别
  3. 前端里有哪些对应场景

可以概括成下面这段意思:

观察者模式是一种一对多依赖关系的设计模式。目标对象维护观察者列表,当自身状态变化时,会直接通知这些观察者更新。它适合处理数据变化带来的联动更新问题,前端里常见于响应式系统、视图更新、表单联动这类场景。它和发布订阅模式的区别在于,观察者模式里目标对象直接知道观察者,而发布订阅模式通常会多一层事件中心做解耦。

如果被追问“它的缺点是什么”,可以补一句:

它的问题主要在于目标对象和观察者之间仍然有直接关系,观察者太多时会增加管理成本;另外如果注册和移除控制不好,也容易造成重复通知和内存问题。

这样讲已经足够自然,也足够像你真的理解过它,而不是只背过定义。


十二、练习题

练习 1:手写一个最小观察者模式

要求:

  • 有一个 Subject
  • 有多个 Observer
  • Subject 状态变化时能通知所有观察者

练习 2:给 Subject 加上移除观察者能力

要求:

  • 支持注册观察者
  • 支持移除观察者
  • 移除后不再收到通知

练习 3:判断下面这些场景更像不像观察者模式

  • 响应式数据变化后触发视图更新
  • 事件总线广播消息给任意模块
  • 表单值变化带动按钮禁用状态更新
  • 插件系统通过中间层分发生命周期事件

可以先自己判断,再参考这个方向:

场景是否更像观察者模式原因
响应式数据变化后触发视图更新典型依赖关系更新
事件总线广播消息给任意模块不太像更接近发布订阅
表单值变化带动按钮状态更新状态变化直接驱动依赖项
插件系统通过中间层分发事件不太像更接近发布订阅

十三、最后做个收束

如果只记一句话,那就是:

观察者模式的重点,是让依赖同一个状态源的多个对象,在状态变化时能够自动更新。

它适合这种场景:

  • 有一个核心状态源
  • 有多个对象依赖这个状态源
  • 状态一变,依赖它的对象就该联动更新

理解到这里,再回头看响应式系统、数据驱动视图、表单联动,你会发现观察者模式并没有那么“书本化”,它其实一直都在前端开发里反复出现。