JS 设计模式应用实践 – 观察者模式与发布订阅的差异与实现

观察者模式中主体直接通知观察者,两者存在耦合;发布-订阅模式通过事件总线解耦发布者与订阅者。1. 观察者模式:主体维护观察者列表并主动调用其更新方法,适用于关系明确、局部通信的场景。2. 发布-订阅模式:引入事件总线作为中间人,发布者与订阅者仅与总线交互,实现完全解耦,适合跨模块、全局通信。3. 现代前端框架如React、Vue的状态管理采用发布-订阅思想,因组件化架构需扁平化通信、异步处理和高解耦。4. 选择依据:若对象间关系紧密且范围小,选观察者模式;若需高度解耦、跨模块通信或未来扩展性强,选发布-订阅模式。5. 发布-订阅虽提升灵活性,但过度使用可能导致事件流混乱、调试困难。

js 设计模式应用实践 - 观察者模式与发布订阅的差异与实现

在JavaScript中,观察者模式(Observer Pattern)和发布-订阅模式(Publish-Subscribe Pattern)都是实现对象间一对多通信的有效方式,但它们在解耦程度和实现机制上存在显著差异。核心观点是:观察者模式中,主体(Subject)直接管理并通知它的观察者(Observer),两者之间存在直接的引用关系;而发布-订阅模式则引入了一个中间的事件调度中心(Event Bus/Broker),发布者(Publisher)和订阅者(Subscriber)都只与这个中心交互,彼此之间完全解耦。

解决方案

在我看来,理解这两种模式的关键在于“谁知道谁”。在观察者模式里,主体是“知道”它的观察者的,它维护着一个观察者列表,并在自身状态变化时,遍历这个列表并直接调用观察者的更新方法。这就像一个明星(主体)直接告诉他的粉丝团(观察者)他的最新动态。

// 观察者模式:主体直接管理观察者class Subject {    constructor() {        this.observers = []; // 主体知道所有观察者    }    addObserver(observer) {        this.observers.push(observer);    }    removeObserver(observer) {        this.observers = this.observers.filter(obs => obs !== observer);    }    notify(data) {        console.log("Subject: My state changed, notifying observers...");        this.observers.forEach(observer => observer.update(data));    }}class Observer {    constructor(name) {        this.name = name;    }    update(data) {        console.log(`${this.name}: Received update - ${data}`);    }}// 使用const mySubject = new Subject();const obsA = new Observer('Observer A');const obsB = new Observer('Observer B');mySubject.addObserver(obsA);mySubject.addObserver(obsB);mySubject.notify('New data available!');// Output:// Subject: My state changed, notifying observers...// Observer A: Received update - New data available!// Observer B: Received update - New data available!

而发布-订阅模式则完全不同。它引入了一个“中间人”——事件总线(Event Bus)或消息代理(Message Broker)。发布者仅仅向这个中间人发布某个“主题”的事件,它根本不知道谁会订阅这个主题。同样,订阅者也只是向中间人订阅它感兴趣的“主题”,它也不知道是哪个发布者发布了这些事件。这就像一个公告板(事件总线),有人在上面贴通知(发布),有人去看自己感兴趣的通知(订阅),发通知的和看通知的彼此不认识。

// 发布-订阅模式:通过事件总线解耦发布者和订阅者class EventBus {    constructor() {        this.subscribers = {}; // topic: [callbacks]    }    subscribe(topic, callback) {        if (!this.subscribers[topic]) {            this.subscribers[topic] = [];        }        this.subscribers[topic].push(callback);        // 返回一个取消订阅的函数,便于管理        return () => {            this.subscribers[topic] = this.subscribers[topic].filter(cb => cb !== callback);        };    }    publish(topic, data) {        if (this.subscribers[topic]) {            console.log(`EventBus: Publishing topic "${topic}" with data:`, data);            this.subscribers[topic].forEach(callback => {                try {                    callback(data);                } catch (error) {                    console.error(`Error in subscriber for topic "${topic}":`, error);                }            });        }    }}// 使用const globalEventBus = new EventBus();// 订阅者const unsubscribeLogger = globalEventBus.subscribe('userLoggedIn', (userData) => {    console.log(`Logger Service: User ${userData.username} logged in.`);});const unsubscribeWelcome = globalEventBus.subscribe('userLoggedIn', (userData) => {    console.log(`Welcome Message: Hello, ${userData.username}!`);});// 发布者function simulateLogin(user) {    console.log(`Simulating login for ${user.username}...`);    globalEventBus.publish('userLoggedIn', user);}simulateLogin({ userId: 1, username: 'Alice' });// Output:// Simulating login for Alice...// EventBus: Publishing topic "userLoggedIn" with data: { userId: 1, username: 'Alice' }// Logger Service: User Alice logged in.// Welcome Message: Hello, Alice!unsubscribeLogger(); // 移除日志服务的订阅simulateLogin({ userId: 2, username: 'Bob' });// Output:// Simulating login for Bob...// EventBus: Publishing topic "userLoggedIn" with data: { userId: 2, username: 'Bob' }// Welcome Message: Hello, Bob!

从代码层面看,最大的区别就是那个

EventBus

。观察者模式里,

Subject

承担了

EventBus

的部分职责,它直接维护了订阅列表。而发布-订阅模式则把这个职责抽离成了一个独立的模块。这种分离带来的就是更高的解耦度。

为什么在现代前端框架中,我们更多地看到类似发布订阅的模式?

这确实是一个很有意思的现象。现代前端框架,如React(通过Context API、Redux/MobX等状态管理库)、Vue(通过Vuex/Pinia等状态管理库,或早期的EventBus模式),它们在组件间通信和状态管理上,都大量借鉴了发布-订阅的思想。究其原因,我认为主要有以下几点:

首先是组件化架构的必然选择。在大型前端应用中,组件树可能非常深,层级复杂。如果采用传统的观察者模式,一个深层嵌套的子组件要通知一个远方的兄弟组件或祖先组件,就需要通过props一层层传递回调函数(props drilling),或者直接引用(这会造成紧耦合)。这很快就会变得难以维护和理解。发布-订阅模式提供了一个“扁平化”的通信渠道,任何组件都可以发布事件,任何组件都可以订阅事件,只要它们约定好事件的“主题”,就可以进行通信,而无需知道彼此的具体位置和引用。

其次是实现真正的解耦和模块化。发布-订阅模式让发布者和订阅者之间没有任何直接依赖。这意味着你可以独立开发、测试和部署不同的模块或组件,只要它们遵守相同的事件协议。一个组件可以发布一个事件,即使当前没有任何订阅者,它也能正常工作。未来如果需要新的功能来响应这个事件,只需要添加一个新的订阅者即可,而无需修改发布者。这种高度解耦对于大型团队协作和长期项目维护至关重要。

再者,状态管理的需求。像Redux、Vuex这样的状态管理库,其核心机制就是发布-订阅模式的变体。当应用的状态(store)发生变化时,它会通知所有订阅了这些状态变化的组件进行更新。组件不需要直接“观察”store,而是通过连接器(connect)或hooks向store“订阅”变化,store则作为发布者,在状态更新时“发布”通知。这使得状态变化可预测、可追溯,并且能高效地更新UI。

最后,异步和跨模块通信的便利性。在复杂的应用中,事件往往需要跨越不同的模块,甚至可能涉及异步操作。发布-订阅模式天生就适合处理这类场景。事件总线可以很容易地集成异步处理机制(例如,将事件放入队列,稍后处理),并且它提供了一个统一的接口来管理所有事件流,这在观察者模式中,如果主体过多,就会变得非常碎片化。

所以,与其说现代框架“模仿”了发布-订阅,不如说它们在实践中自然而然地演化出了这种高度解耦的通信机制,因为这是解决复杂前端应用通信挑战的优雅之道。

如何选择:观察者模式还是发布订阅模式?

选择这两种模式,在我看来,主要取决于你对耦合度系统规模的容忍度和需求。没有绝对的优劣,只有更适合特定场景的选择。

我通常会这样考虑:

选择观察者模式的场景:

紧密耦合且关系明确的一对多关系: 如果你有一个明确的“主体”对象,它直接拥有并管理它的“观察者”列表,并且主体和观察者之间的关系是直接且强烈的,那么观察者模式可能更合适。例如,一个数据模型(Subject)需要通知所有直接依赖它的UI组件(Observer)进行更新。主体明确知道谁在观察它。小范围、局部性的通信: 当通信范围仅限于少数几个对象之间,且这些对象都在同一个模块或组件的紧密控制之下时,观察者模式的实现会更简单、更直接,开销也更小。同步通知是预期行为: 观察者模式通常意味着主体状态变化后,会立即、同步地通知所有观察者。如果这是你希望的行为,那么它很合适。示例:DOM元素上的事件监听器:一个按钮(Subject)直接管理它的点击处理函数(Observer)。一个自定义表单控件(Subject)通知其父级表单(Observer)其值已更改。

选择发布-订阅模式的场景:

高度解耦是首要目标: 如果你希望发布者和订阅者之间完全不知道彼此的存在,只通过一个中间人进行通信,那么发布-订阅模式是理想选择。这对于构建可插拔、可扩展的系统至关重要。跨模块、全局性的通信: 当事件需要跨越整个应用程序的不同部分,或者在完全不相关的组件之间进行通信时,一个全局的事件总线能提供一个统一、清晰的通信渠道。异步处理事件的需求: 虽然发布-订阅模式本身不强制异步,但事件总线更容易集成异步处理机制(例如,将事件推入队列,稍后处理),这对于处理耗时操作或避免UI阻塞非常有用。未来扩展性考虑: 如果你预计未来会有新的功能需要响应现有事件,或者新的模块需要发布事件,而不想修改现有代码,发布-订阅模式能提供更大的灵活性。示例:用户登录后,需要通知导航栏更新、分析服务记录、欢迎消息显示等多个不相关的模块。一个第三方插件发布特定事件,而你的应用可以订阅这些事件来扩展功能。大型单页应用中的状态管理,如Redux或Vuex,它们通过发布-订阅机制通知组件状态变化。

我的经验是,对于小型、简单且耦合关系明确的场景,观察者模式足够用,甚至更直接。但对于复杂、大型应用,特别是需要高度模块化和解耦的前端项目,发布-订阅模式的优势会非常明显,它能有效降低系统复杂度,提升可维护性。当然,过度使用发布-订阅也可能导致事件流难以追踪,增加调试难度

以上就是JS 设计模式应用实践 – 观察者模式与发布订阅的差异与实现的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1521943.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 14:36:53
下一篇 2025年12月19日 22:07:33

相关推荐

  • 使用CSS Transition实现平滑Navbar显示/隐藏效果

    本文旨在提供一种使用CSS Transition和JavaScript结合的方式,实现Navbar平滑显示和隐藏效果的教程。通过添加CSS过渡效果和JavaScript的类切换功能,可以创建一个流畅的用户体验,避免生硬的显示/隐藏切换。本文将提供详细的代码示例和步骤说明,帮助开发者轻松实现这一效果。…

    好文分享 2025年12月20日
    000
  • 如何用Performance API监控网页运行时性能?

    Performance API通过window.performance提供页面加载、资源消耗及用户体验指标,利用getEntriesByType、mark/measure和PerformanceObserver监控关键性能数据,并结合批处理与异步上报优化收集效率。 Performance API是现…

    2025年12月20日
    000
  • 如何用JavaScript实现一个简单的操作系统模拟器?

    答案:JavaScript通过数据结构和事件循环模拟进程调度与内存管理。用数组实现就绪队列,setInterval触发时间片轮转,进程执行指令改变状态;物理内存用Array模拟,Map记录分配情况,进程申请时查找空闲块,终止时释放内存。 用JavaScript实现一个简单的操作系统模拟器,核心在于模…

    2025年12月20日
    000
  • JS 前端自动化测试 – 端到端测试与视觉回归测试的实践方案

    前端自动化测试已成为保障产品质量和用户体验的基石,E2E测试确保业务流程功能正确,视觉回归测试保障UI一致性。 在前端开发日益复杂的今天,JS前端自动化测试,特别是端到端(E2E)测试与视觉回归测试,已不再是可有可无的选项,而是确保产品质量和用户体验的基石。它们从不同维度保障应用:E2E测试关注用户…

    2025年12月20日
    000
  • 解决Next.js本地字体在Vercel部署时解析失败的问题

    本文旨在解决Next.js应用在使用next/font/local引入本地字体时,在本地开发环境运行正常,但在Vercel部署时出现“Module not found”错误的问题。核心解决方案在于遵循严格的文件和目录命名规范,即避免在字体文件或其所在目录的名称中使用空格和大写字母,以确保跨平台的文件…

    2025年12月20日
    000
  • 如何利用Mutation Observer监听DOM变化,以及它在实现自动化测试或UI同步时的最佳实践?

    Mutation Observer能异步高效监听DOM变化,适用于自动化测试中解决元素加载时序问题和竞态条件。通过创建实例并配置观察选项,可精准捕获节点增删、属性或文本变化,在回调中实现响应逻辑。相比事件委托,它能监听结构化变更,避免轮询,提升性能。在自动化测试中可封装为waitForElement…

    2025年12月20日
    000
  • JS 模块热替换原理 – Webpack 运行时模块更新机制的技术内幕

    Webpack HMR核心机制是通过WDS与HMR Runtime协同,利用WebSocket通知、按需编译和模块级替换实现无刷新更新;其通过module.hot API管理状态与副作用,在保留应用状态的同时动态替换代码,提升开发效率。 JavaScript模块热替换(HMR)本质上是Webpack…

    2025年12月20日
    000
  • Nuxt.js中从Vuex Action程序化重定向到错误页面的指南

    本教程详细介绍了如何在Nuxt.js应用中,特别是从Vuex action的catch块内,程序化地将用户重定向到自定义错误页面。文章将演示如何利用this.$nuxt.error()方法传递错误状态码和消息,并说明如何在error.vue页面中访问这些信息以提供友好的用户反馈,同时提供代码示例和最…

    2025年12月20日
    000
  • JS 代码混淆与保护 – 防止逆向工程的各种加密方案优缺点分析

    JavaScript代码混淆的主要技术手段包括:1. 标识符重命名,将有意义的变量函数名替换为无意义字符,降低可读性;2. 字符串字面量加密,运行时解密关键字符串,防止敏感信息泄露;3. 控制流扁平化,打乱代码执行逻辑,增加分析难度;4. 冗余代码注入,插入无用代码干扰逆向分析;5. 反调试与反篡改…

    2025年12月20日
    000
  • JavaScript日期处理库的封装与优化

    封装JavaScript日期处理库的核心是通过设计统一、高效、可维护的API来提升开发效率与代码健壮性。文章首先提出封装的本质是建立标准化工具集,涵盖格式化、解析、加减、比较等核心功能,并以DateUtil为例展示如何通过函数封装实现基础操作。接着强调优化需从性能(如减少new Date()调用)、…

    2025年12月20日
    000
  • 如何实现JavaScript中的函数重载?

    JavaScript无原生函数重载,因动态类型特性导致同名函数被覆盖,但可通过arguments判断参数数量或类型模拟重载;ES6+引入默认参数、剩余参数和对象解构等特性,使函数能更优雅地处理多样输入,提升灵活性与可读性;实践中应避免过多if-else判断以防止可读性下降,推荐使用参数对象模式或分发…

    2025年12月20日
    000
  • 如何用WebHID API接入人体学输入设备?

    WebHID API支持浏览器直接与HID设备通信,解决传统Web无法访问非标准硬件的痛点。通过用户主动触发requestDevice()选择设备,结合getDevices()实现重新连接,开发者可构建如定制外设配置、辅助技术、工业控制等创新应用,同时需注重权限安全与用户体验设计。 WebHID A…

    2025年12月20日
    000
  • JS 模块打包原理剖析 – 从 CommonJS 到 Tree Shaking 的工作机制

    JS模块打包通过整合分散的文件与依赖,解决全局变量冲突、依赖混乱及HTTP请求过多等问题,提升性能与开发效率。它利用Tree Shaking消除未使用代码,依赖静态分析实现优化,并兼容CommonJS与ES Modules,通过转换、合并、压缩等手段输出高效可运行的静态资源。 JS模块打包,在我看来…

    2025年12月20日
    000
  • 实现平滑过渡效果的导航栏显示与隐藏

    本文旨在提供一种使用 CSS 过渡和 JavaScript 类切换,为导航栏添加平滑显示与隐藏效果的实用方法。通过修改 CSS 属性(如 opacity 和 transform)并结合 JavaScript 的事件监听,可以轻松实现导航栏的动画效果,提升用户体验。本文将详细介绍具体实现步骤,并提供完…

    2025年12月20日
    000
  • 如何通过Proxy和Reflect实现元编程,以及这些特性在框架开发中的实际作用是什么?

    Proxy和Reflect通过拦截并自定义对象操作,实现响应式数据绑定与ORM等高级功能。Proxy创建代理对象,拦截属性读写、方法调用等操作,结合Reflect转发默认行为,确保this正确性与操作安全性。在Vue 3中,Proxy替代Object.defineProperty,解决动态增删属性监…

    2025年12月20日
    000
  • 限制 React 输入框数值范围:一个详细教程

    在 React 应用中,经常需要限制输入框的数值范围,以确保用户输入的数据符合预期。例如,一个年龄输入框可能需要限制在 0-120 之间。本文将介绍如何使用 onBlur 事件处理程序和 Math.min、Math.max 函数来实现这一功能。 使用 onBlur 事件处理程序限制输入范围 onBl…

    2025年12月20日
    000
  • 如何通过JavaScript实现滑动门效果?

    滑动门效果通过CSS transition和JavaScript控制元素宽高实现,常用于导航菜单、信息展示等场景,性能优化需避免频繁重排、使用GPU加速及节流防抖技术。 滑动门效果,简单来说,就是鼠标悬停或点击时,内容区域像门一样滑开或滑入,显示更多信息。JavaScript实现的核心在于动态改变元…

    2025年12月20日
    000
  • TestRail中筛选自动化测试用例并添加到测试运行的教程

    本教程详细介绍了如何通过TestRail API筛选出具有特定自定义字段(例如“可自动化”)的测试用例,并将其添加到现有的测试运行中。文章将分步指导如何使用get_cases API获取测试套件中的所有用例,解析JSON响应以识别符合条件的用例ID,然后利用update_run API将这些筛选出的…

    2025年12月20日
    000
  • 在React中实现带有min/max限制的受控数字输入组件

    本文详细讲解如何在React中创建一个受控的数字输入组件,使其值严格遵守父组件传递的min和max属性限制。通过利用onBlur事件进行值钳制,并优化增减按钮的逻辑,确保用户输入和交互始终在有效范围内,从而提升组件的健壮性和用户体验。 在React应用开发中,我们经常需要构建可复用的表单组件。当涉及…

    2025年12月20日
    000
  • 如何通过JavaScript实现进度条效果?

    进度条通过HTML、CSS和JavaScript实现,核心是JS动态更新元素宽度以反映进度。HTML构建容器与填充条,CSS设置样式并用transition实现平滑动画,JS计算进度并更新DOM。为提升体验,可添加动画效果、丰富文本提示、状态反馈及ARIA属性增强无障碍访问。常见于文件上传、数据加载…

    2025年12月20日
    000

发表回复

登录后才能评论
关注微信