ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

前端精读周刊:Observer 观察者模式——一对多依赖通知机制的意图、实现与前端源码印证

前端精读周刊:Observer 观察者模式——一对多依赖通知机制的意图、实现与前端源码印证 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/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内部调用notifynotify遍历所有监听者包括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 模式描述的是「通过代理对象代替原始对象的访问」这一实现手段而观察者模式描述的是「一对多依赖下如何通知更新」这一行为目标。两者一个是实现层一个是概念层可以组合使用。原文给出的告诫值得反复体会使用设计模式切记不要死板理解原理就行了在不同平台有不同的更加优雅的实现方式。源码级印证观察者模式在真实前端数据流中的形态观察者模式不是理论空谈前端数据流框架几乎全部建立在这一模式之上。本仓库的 源码解读 模块收录了多篇数据流框架源码精读下面逐一印证。zustandlistenerssubscribesetState的教科书实现精读《zustand 源码》 展示了 zustand 核心createStore的实现其结构与上面的Subject类如出一辙监听者容器是一个Setconst listeners: SetStateListenerTState new Set()setState做了两件事修改state并执行所有 listenerconst setState: SetStateTState (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返回的 storeaddObserver注册subscribe(listener)removeObserver注销destroy/unsubscribenotify广播listeners.forEach(listener listener(...))Update观察者动作React 侧的listener内部触发forceUpdate值得注意的是zustand 在setState中还加入了一层优化只有nextState ! state时才触发广播这是对朴素观察者模式的工程化增强避免无意义的状态变更引发全量通知。SolidJScreateSignalcreateEffect的依赖收集式观察者精读《SolidJS》 展示了另一种观察者形态——依赖收集const App () { const [count, setCount] createSignal(0); createEffect(() { console.log(count()); // 在 count 变化时重新执行 }); };这里的createEffect就是观察者它读取了count()便自动建立了对count的依赖当count变化时createEffect的回调自动重新执行。关键差异在于传统观察者模式需要显式注册addObserverSolidJS 通过依赖收集实现隐式注册——在 effect 回调执行期间读取了哪些 signal就自动成为哪些 signal 的观察者。这正是原文「不要拘泥于实现形式」的极致体现观察者的「一对多、自动通知、自动更新」行为目标没有变但注册方式从「手动」进化成了「自动」。dobReaction将观察者拆成「依赖收集」与「触发回调」精读《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-stateview包裹下的自动订阅与批量更新精读《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把普通对象包装为可观察对象Subjectview包裹组件利用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前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐前端精读周刊前端异步编程模式前端精读周刊前端异步编程模式 引言 你是否还在为回调地狱而烦恼是否在Promise链式调用中迷失方向是否对async/await的性能陷阱感到困惑本文将文档技术博客教程别再手动点了青龙面板 API 批量运维全流程别再手动点了青龙面板 API 批量运维全流程 凌晨两点一批定时任务要在整点前停掉你只能打开青龙面板的网页一个个点想改脚本里的账号配置又要翻到环境变量页任务调度后端前端5大核心技术揭秘Video2X如何用C重构实现视频超分辨率与帧插值突破5大核心技术揭秘Video2X如何用C重构实现视频超分辨率与帧插值突破 Video2X作为一款基于机器学习的视频超分辨率与帧插值框架在6.0.0版本中通音视频视频处理图像处理深度学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表