- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
Observer(观察者模式)属于行为型模式,它回答了一个几乎所有前端项目都会遇到的核心问题:当一份数据被多个视图、多个模块共同依赖时,如何让数据变化自动驱动所有依赖方完成更新。本篇以「前端精读周刊」设计模式系列第 185 期为骨架,完整梳理观察者模式的意图、生活化案例、TypeScript 最小实现,并结合本仓库对 zustand、react-easy-state、SolidJS、dob 等真实前端数据流库的源码解读,印证观察者模式在生产级框架中的落地形态,帮助你做到「看懂模式、写出实现、读得懂源码」。
Observer 模式定位与核心意图
在 GoF 设计模式的分类中,Observer 属于行为型模式。行为型模式关注的是对象之间的职责分配与通信方式,而观察者模式正是其中最常用、覆盖面最广的一种通信机制。
其意图可以概括为一句话:
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
这句话拆开看有三个关键点:
- 一对多:一个「目标(Subject)」对应多个「观察者(Observer)」;
- 得到通知:目标状态变化时,不主动去调用某个具体对象,而是广播式地通知所有注册者;
- 自动更新:观察者收到通知后自行完成刷新动作,目标无需关心观察者内部实现。
一个来自 npm 的现实例子
原文用一个非常贴近前端日常的场景解释了这一意图:npm 包与项目之间就是一对多关系——一个 npm 包会被成百上千个项目依赖。当这个包发布新版本时,如果所有依赖它的项目都能「得到通知」并「自动更新」自己的package.json版本号,那么就解决了包版本更新的难题。
虽然现实中 npm 的版本更新机制并不完全等于观察者模式(更接近 registry 的轮询/订阅模型),但这个例子精准地传达了观察者模式的本质:依赖方不需要主动反复询问「你变了吗」,而是被依赖方在变化时主动通知所有依赖方。这比轮询更高效,也比硬编码的逐个调用更解耦。
三个贴近工作的场景举例
设计模式的价值在于「在工作中用起来」。原文准备了三个场景,分别对应前端、实时协作、消息通信三类典型问题。
对象与视图双向绑定
这是观察者模式最初被提出的场景,也是前端框架的核心问题之一。
假设同一份数据需要同时渲染为表格与柱状图两种视图:当用户在表格中编辑数据后,如何让柱状图自动刷新?
这里的数据与 UI 就是典型的一对多关系:
- 「数据」是目标(Subject);
- 「表格」和「柱状图」是观察者(Observer)。
观察者模式给出的答案是:表格编辑触发数据setState→ 数据更新后notify所有注册的观察者 → 表格与柱状图各自执行Update刷新自己。这样数据层不需要知道具体有哪些视图,视图也不需要相互引用,新增一个「折线图」视图只需注册一次即可。
本仓库的 精读《设计模式 - Proxy 代理模式》 中也提到了双向绑定概念,但两者定位不同:代理是实现双向绑定的一个具体方案,而观察者模式才是在描述双向绑定这个概念本身。模式是「问题域」的抽象,代理只是「实现域」的一种手段。
拍卖
拍卖由一名拍卖员与多位竞价者组成,这个场景与观察者模式几乎一一对应:
- 竞价者 A 喊出「我出 100」,本质是观察者向目标发出的
setState更新请求; - 拍卖员喊出「有人出价 100,还有更高的吗?」,本质是一次
notify通知行为; - 拍卖员通知现场竞价全员,刷新他们对「当前最高价」的信息。
这里的「最高价」就是 Subject 的状态,竞价者是 Observer,拍卖员则承担了 Subject 的「维护观察者列表 + 广播通知」职责。
聊天室
聊天室由中央服务器与多个客户端组成,同样是一个教科书级的观察者场景:
- 客户端发送消息 = 向中央服务器发送
setState更新请求; - 中央服务器通知同一聊天室的所有客户端 =
notify; - 各客户端收到通知后更新自己的消息列表 =
Update。
聊天室场景与前端数据流的映射几乎是无缝的:中央服务器就是全局 store,客户端就是订阅了 store 的各个组件。
意图解释
数据与 UI 双向绑定的例子已经完整说明了观察者模式的意图:把「状态变化」与「依赖方更新」解耦,通过注册与广播代替硬编码的逐项调用。这样带来的收益是:
- 扩展性:新增观察者无需修改目标代码,只需注册;
- 复用性:目标不依赖任何具体观察者,可被任意场景复用;
- 一致性:所有依赖方在同一轮通知中获取到相同的最新状态,避免数据不同步。
结构图与协作流程
原文给出了模式的两张核心图(结构图与协作图),这里用文字还原其要点,便于在没有配图的场景下理解。
两个核心角色
- Subject(目标):即例子中的「数据」。它维护一份观察者列表,提供注册、注销与通知能力,并在自身状态变化时触发广播。
- Observer(观察者):即例子中的「表格」「柱状图」。它注册到 Subject 上,并实现统一的
Update方法,收到通知后自行刷新。
一次完整的协作流程
以数据与 UI 同步为例,原文描述了这样的调用链:
- 表格发生操作修改数据,表格这个
TableObserver调用 Subject(数据)的setState; - 数据被更新,同时
setState内部调用notify; notify遍历所有监听者(包括TableObserver与ColumnChartObserver),依次调用它们的Update方法;- 每个观察者的
Update方法内部调用getState获取最新数据,完成各自视图的刷新。
即:表格更新 → 更新数据 → 表格、柱状图同时刷新。
协作图中的三个具体对象
aConcreteSubject:对应例子中的「数据」,是 Subject 的具体实现;aConcreteObserver:对应例子中的「表格」;anotherConcreteObserver:对应例子中的「柱状图」。
协作图展示的正是观察者模式的运行时行为:多个具体观察者围绕一个具体目标,目标的状态变更通过通知机制扩散到所有观察者。理解了这张时序,就理解了观察者模式的全部运行机理。
TypeScript 最小实现
下面是原文提供的 TypeScript 实现。为了简化处理,不定义 Subject 接口与 ConcreteSubject,而是直接用Subject类代替,Observer同理。完整代码与逐段注释如下:
// 目标,管理所有观察者 class Subject { // 观察者数组 private observers: Observer[] = [] // 状态 private state: State // 通知所有观察者 private notify() { this.observers.forEach(eachObserver => { eachObserver.update() }) } // 新增观察者 public addObserver(observer: Observer) { this.observers.push(observer) } // 更新状态:赋值后立即广播,保证所有观察者同步到最新值 public setState(state: State) { this.state = state this.notify() } // 读取状态:观察者更新时通过它拿到最新数据 public getState() { return this.state } } // 观察者 class Observer { // 维护目标:观察者需要知道自己监听的是哪个目标 private subject: Subject constructor(subject: Subject) { this.subject = subject // 关键:构造时自动注册,观察者与被观察者在此建立一对多关系 this.subject.addObserver(this) } // 更新:收到通知后执行,比如渲染表格 or 渲染柱状图 public update() { console.log(this.subject.getState()) } } // 客户端调用 const subject = new Subject() // 创建观察者(构造时即完成注册) const observer1 = new Observer(subject) const observer2 = new Observer(subject) // 更新状态:observer1 与 observer2 都会收到通知并执行 update subject.setState(10)逐段理解这段代码
- Subject 内部维护
observers: Observer[]数组,这是「一对多」中「多」的载体;状态state是通知的「消息内容」。 notify()是广播的入口:遍历观察者数组并逐个调用update()。它被设计为private,只在setState内部触发,保证「状态变更必然伴随通知」这一不变式。setState是外部唯一的写入入口:先更新状态,再广播,顺序至关重要——必须先让getState能读到新值,观察者的update才能取到正确数据。- Observer 构造时调用
subject.addObserver(this)完成注册:这体现了观察者模式的耦合方式——观察者主动依附于目标,目标对观察者一无所知(它只调用统一的update()接口)。 - 客户端只需两步:创建目标 → 创建观察者(自动注册)→
setState触发全员更新。业务方完全不需要手动管理「谁依赖谁」的调用链。
可扩展方向
原文代码是最小可运行版本,在实际工程中通常还会补充两点(以下为基于原代码的教学延伸,非仓库原文内容):
- 注销能力:新增
removeObserver(observer),将观察者从数组中移除,用于组件卸载时解除监听,避免内存泄漏与重复通知; - 多状态通知:
notify时传入变化的状态片段(如eachObserver.update(this.state)),让观察者按需消费,减少无谓的重复读取。
不要拘泥于实现形式:用 Proxy 实现观察者
原文特别强调:观察者模式的价值在于描述一对多通知这一概念,而非某种固定代码组织形式。subject与observer1、observer2是一对多关系,但不一定非要用「观察者数组 + 手动注册」来实现,利用 ES6 的Proxy可以更优雅地达成同样的效果:
const obj = new Proxy(obj, { get(target,key) {} set(target,key,value) {} }) renderTable(obj) renderChart(obj)核心思路:
- 在
obj被任意组件访问时触发get,进而对 UI 与视图进行绑定(相当于隐式注册观察者); - 在
obj被任意组件更新时触发set,进而对所有使用到的视图进行刷新(相当于隐式广播通知)。
Proxy 方案与「观察者数组」方案的关系,正好呼应了本仓库 精读《设计模式 - Proxy 代理模式》 的结论:Proxy 模式描述的是「通过代理对象代替原始对象的访问」这一实现手段,而观察者模式描述的是「一对多依赖下如何通知更新」这一行为目标。两者一个是实现层,一个是概念层,可以组合使用。
原文给出的告诫值得反复体会:使用设计模式切记不要死板,理解原理就行了,在不同平台有不同的更加优雅的实现方式。
源码级印证:观察者模式在真实前端数据流中的形态
观察者模式不是理论空谈,前端数据流框架几乎全部建立在这一模式之上。本仓库的 源码解读 模块收录了多篇数据流框架源码精读,下面逐一印证。
zustand:listeners+subscribe+setState的教科书实现
精读《zustand 源码》 展示了 zustand 核心createStore的实现,其结构与上面的Subject类如出一辙:
- 监听者容器是一个
Set:
const listeners: Set<StateListener<TState>> = new Set()setState做了两件事:修改state并执行所有 listener:
const setState: SetState<TState> = (partial, replace) => { const nextState = typeof partial === 'function' ? partial(state) : partial if (nextState !== state) { const previousState = state state = replace ? (nextState as TState) : Object.assign({}, state, nextState) listeners.forEach((listener) => listener(state, previousState)) } }- 注册与注销时机分别是
subscribe与destroy函数调用时:subscribe时注册的监听函数会作为listener添加到listeners队列中,当发生setState时便会被调用。
这与观察者模式的对应关系一目了然:
| 观察者模式概念 | zustand 实现 |
|---|---|
| Subject(目标) | createStore返回的 store |
| addObserver(注册) | subscribe(listener) |
| removeObserver(注销) | destroy/unsubscribe |
| notify(广播) | listeners.forEach(listener => listener(...)) |
| Update(观察者动作) | React 侧的listener(内部触发forceUpdate) |
值得注意的是,zustand 在setState中还加入了一层优化:只有nextState !== state时才触发广播,这是对朴素观察者模式的工程化增强,避免无意义的状态变更引发全量通知。
SolidJS:createSignal+createEffect的依赖收集式观察者
精读《SolidJS》 展示了另一种观察者形态——依赖收集:
const App = () => { const [count, setCount] = createSignal(0); createEffect(() => { console.log(count()); // 在 count 变化时重新执行 }); };这里的createEffect就是观察者:它读取了count(),便自动建立了对count的依赖;当count变化时,createEffect的回调自动重新执行。关键差异在于:
- 传统观察者模式需要显式注册(
addObserver); - SolidJS 通过依赖收集实现隐式注册——在 effect 回调执行期间读取了哪些 signal,就自动成为哪些 signal 的观察者。
这正是原文「不要拘泥于实现形式」的极致体现:观察者的「一对多、自动通知、自动更新」行为目标没有变,但注册方式从「手动」进化成了「自动」。
dob:Reaction将观察者拆成「依赖收集」与「触发回调」
精读《dob - 框架实现》 从更底层剖析了 MVVM 依赖追踪的核心。原文指出:
依赖追踪分为两部分,分别是依赖收集与触发回调,如果把这两个功能合起来,就是
observe函数,分开的话,就是较为底层的Reaction。
function observe(callback) { const reaction = new Reaction(() => { reaction.track(callback) }) reaction.run() }reaction.run()在初始化就执行回调,回调中用到的变量被记录(依赖收集);当变量更改时,触发Reaction的回调,重新收集一轮依赖,同时执行callback(触发回调)。变量(Subject)与回调函数(Observer)的绑定在这一收一发的循环中自动建立并持续更新。
dob 还揭示了观察者模式在依赖追踪场景的一个经典限制:依赖收集无法得到触发时的环境信息,因此同步函数可以正确绑定,而异步或嵌套函数容易发生依赖错绑。这也是为什么多数响应式框架要求observe回调保持同步。
react-easy-state:view包裹下的自动订阅与批量更新
精读《react-easy-state 源码》 展示了观察者模式与 React 组件的结合方式:
import { store, view } from "react-easy-state"; const counter = store({ num: 0 }); const increment = () => counter.num++; export default view(() => <button onClick={increment}>{counter.num}</button>);其内部流程与观察者模式一一对应:
store把普通对象包装为可观察对象(Subject);view包裹组件,利用observe(Comp, { scheduler, lazy: true })建立组件(Observer)与 store 的订阅关系;- 任何对 store 的修改都会触发
scheduler中注册的forceUpdate,驱动组件重渲染(Update); - 组件卸载时通过
useEffect返回的清理函数执行unobserve(render)解除订阅(removeObserver)。
react-easy-state 还引入了batch机制:利用unstable_batchedUpdates保证批量修改只触发一次渲染。这解决了朴素观察者模式的经典痛点——连续多次setState导致多次无意义广播,与 zustand 的nextState !== state判断属于同一类工程优化。
总结
观察者模式是使用频率极高的行为型设计模式,它描述了对象一对多依赖关系下「如何通知、如何更新」的机制。这种机制的应用范围远超前端:
- 前端的 UI 与数据映射:表格、柱状图、任意组件订阅同一份数据;
- 后端的请求与控制器映射:多个处理器订阅同一个事件源;
- 平台间的消息通知:聊天室、消息队列、事件总线;
- 现实世界的任何「依赖 + 通知」场景:拍卖、订阅制、发布通知。
无论现实还是程序,存在依赖且需要通知的场景非常普遍,这正是观察者模式历久弥新的原因。
从本仓库的源码解读可以看出,观察者模式的理念已经深深渗透进前端数据流的每一层实现:zustand 的listeners与subscribe是显式注册的经典形态,SolidJS 的createSignal/createEffect是依赖收集的自动形态,dob 的Reaction揭示了底层「依赖收集 + 触发回调」的机理,react-easy-state 则示范了与 React 组件生命周期结合时的注册与注销实践。
最后再次强调原文的告诫:观察者模式的关键不是背下代码模板,而是识别「一对多依赖需要同步更新」的场景,并选择当下最优雅的实现方式。看懂模式、写出实现、读得懂源码,三者合一,才算真正掌握这门设计模式。
延伸思考
在实际工程中,观察者模式常与「发布订阅(Pub/Sub)」模式对比(此为设计模式通用知识,供延伸阅读):观察者模式中目标与观察者互相感知、直接通信;发布订阅则在两者之间增加消息代理(Broker),实现完全解耦。前端的事件总线、Redux、消息中间件等更接近发布订阅形态。理解这一差异,有助于在「紧耦合通知」与「完全解耦通信」之间做出正确取舍。
- 文档
- 技术博客
- 教程
【免费下载链接】weekly
前端精读周刊。帮你理解最前沿、实用的技术。
相关推荐
前端精读周刊:前端异步编程模式
前端精读周刊:前端异步编程模式 引言 你是否还在为回调地狱而烦恼?是否在Promise链式调用中迷失方向?是否对async/await的性能陷阱感到困惑?本文将
文档技术博客教程别再手动点了:青龙面板 API 批量运维全流程
别再手动点了:青龙面板 API 批量运维全流程 凌晨两点,一批定时任务要在整点前停掉,你只能打开青龙面板的网页一个个点;想改脚本里的账号配置,又要翻到环境变量页
任务调度后端前端5大核心技术揭秘:Video2X如何用C++重构实现视频超分辨率与帧插值突破
5大核心技术揭秘:Video2X如何用C++重构实现视频超分辨率与帧插值突破 Video2X作为一款基于机器学习的视频超分辨率与帧插值框架,在6.0.0版本中通
音视频视频处理图像处理深度学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考