首页/文章/八股文

发布订阅模式 vs 观察者模式

2025-08-06
20707 分钟
...

发布订阅模式 vs 观察者模式:它们真的是一回事吗?

你在学习前端开发时是否也曾困惑:发布订阅和观察者模式听起来如此相似,它们究竟有什么区别?为什么有些资料说它们是一回事,而实际代码实现却截然不同?今天,我们将揭开这两种模式的神秘面纱。

为什么我们需要设计模式?

在深入探讨之前,让我们思考一个问题:为什么我们需要这些设计模式? 想象你正在开发一个复杂的前端应用,多个组件需要相互通信,但又不能紧密耦合在一起。就像城市中的交通系统,如果每辆车都直接与其他车辆通信,那将是灾难性的混乱。设计模式就是为此而生的通信规则手册,让我们的代码保持清晰、可维护和可扩展。

发布订阅模式:事件驱动的消息中心

生活中的类比

想象你订阅了一个技术博客的邮件列表。当博客发布新文章时,所有订阅者都会收到邮件通知。有趣的是,博客作者并不知道具体有哪些订阅者,他们只需要将文章发布到平台上,平台负责通知所有人。

发布订阅模式正是基于这样的思想:发布者和订阅者之间通过一个事件中心进行通信,彼此不直接接触

让我们看一个具体的代码实现:

class EventEmitter {
    constructor() {
        this.eventList = {}
    }

    
    on(eventName, callBack) {
        if (!this.eventList[eventName]) {
            this.eventList[eventName] = [];
        }
        this.eventList[eventName].push(callBack)
    }

    
    emit(eventName) {
        if (this.eventList[eventName]) {
            const callBacks = this.eventList[eventName].slice()
            callBacks.forEach((item) => {
                item()
            })
        }
    }

    
    off(eventName, callBack) {
        if (this.eventList[eventName]) {
            this.eventList[eventName] = this.eventList[eventName].filter((item) => {
                return item !== callBack
            })
        }
    }

    
    once(eventName, callBack) {
        let onceCallBack = () => {
            callBack()
            this.off(eventName, onceCallBack)
        }
        this.on(eventName, onceCallBack)
    }
}

这个 EventEmitter 类就是我们的"事件中心"。它维护着一个 eventList 对象,用来存储所有的事件和对应的回调函数。当我们调用 on 方法时,就是在订阅某个事件;调用 emit 方法时,就是在发布事件。

发布订阅模式的实际应用

function fetchData() {
    setTimeout(() => {
        console.log('数据加载完成');
        _event.emit('data-ready');  
    }, 1000)
}

function renderUI() {
    setTimeout(() => {
        console.log('UI渲染完成');
    }, 500)
}


const _event = new EventEmitter();

fetchData();
_event.on('data-ready', renderUI);  

在这个例子中:

  1. fetchData 完成后发布 data-ready 事件
  2. renderUI 订阅了该事件,在事件触发时执行
  3. 两个函数完全不知道对方的存在,通过事件中心解耦

发布订阅的三大优势

  1. 完全解耦:发布者和订阅者互不知晓对方
  2. 动态管理:可随时添加/移除订阅者
  3. 多对多关系:一个事件可以有多个订阅者,一个订阅者可关注多个事件

观察者模式:直接通知的"点名系统"

生活中的类比

想象一个班主任在教室里宣布通知。老师清楚地知道班上每个学生,通知时会直接看向每个学生。这里,老师(主题)和学生(观察者)是直接关联的。

技术实现

让我们通过一个实际的DOM操作例子来理解观察者模式:

<script>
    let h2 = document.querySelector('h2')
    let btn = document.querySelector('button')
    let obj = {
        count: 1
    }
    let num = obj.count 

    function observer() {
        
        
        h2.innerHTML = num
    }

    Object.defineProperty(obj, 'count', {
        get() {
            return num
        },
        set(newValue) {
            num = newValue
            observer()  
        }
    })

    btn.addEventListener('click', () => {
        obj.count++
    })
</script>

在这个例子中,我们使用了 Object.defineProperty 来监听 count 属性的变化。当 count 发生改变时,setter 函数会直接调用 observer 函数来更新页面显示。这就是典型的观察者模式:主题(obj.count)直接通知观察者(observer函数)。

Vue响应式系统的秘密

观察者模式是Vue响应式系统的核心:

const data = { count: 1 };

Object.defineProperty(data, 'count', {
    get() {
        return this._count;
    },
    set(newValue) {
        this._count = newValue;
        updateView();  
    }
});

function updateView() {
    console.log('视图更新了!');
}

data.count = 5; 

关键点:当数据变化时,Vue直接调用所有依赖该数据的观察者(如视图渲染函数),不需要中间的事件中心。

两种模式的核心差异

特征发布订阅模式观察者模式
通信方式通过事件中心间接通信主题直接通知观察者
耦合度完全解耦主题需维护观察者列表
关系多对多一对多
灵活性高(支持复杂事件处理)中(适合简单通知)
复杂度较高(需实现事件中心)较低(直接维护列表)
典型应用全局事件总线、模块间通信数据绑定、响应式系统

如何选择?实际场景分析

何时选择发布订阅模式

  1. 跨组件通信:在React应用中,使用事件总线实现非父子组件通信

    eventBus.emit('user-logged-in', userData);
    
    
    eventBus.on('user-logged-in', (user) => {
      
    });
    
  2. 微服务架构:不同服务通过消息队列(事件中心)通信

  3. 插件系统:核心系统发布事件,插件订阅感兴趣的事件

何时选择观察者模式

  1. 数据绑定:如Vue的响应式系统

    new Vue({
      data: { message: 'Hello' },
      watch: {
        message(newVal) { 
          console.log('消息变化了:', newVal);
        }
      }
    })
    
  2. 状态管理:Redux中的store通知所有订阅的组件

  3. DOM事件:浏览器内置的事件系统

    button.addEventListener('click', handler); 
    

性能与复杂度权衡

发布订阅模式在大型系统中优势明显:

  • 支持更复杂的事件处理(过滤、转换、优先级)
  • 完全解耦使系统更易扩展
  • 但引入中间层带来轻微性能开销

观察者模式在简单场景更高效:

  • 直接通知减少中间环节
  • 实现简单明了
  • 但当观察者数量巨大时,直接遍历列表可能成为性能瓶颈

常见误区澄清

误区1:“浏览器事件是发布订阅”

❌ 实际上,浏览器的addEventListener观察者模式的实现:

  • DOM元素(主题)直接维护监听器列表
  • 事件触发时直接调用所有监听器

误区2:“Vue的EventBus是观察者模式”

❌ 实际上,Vue的EventBus是发布订阅的典型应用:

const EventBus = new Vue();


EventBus.$emit('data-updated', payload);


EventBus.$on('data-updated', handleData);

这里没有直接依赖,通过Vue实例作为事件中心通信。

总结:根据场景选择最佳方案

理解两种模式的核心区别后,我们可以得出以下实践建议:

  1. 当你需要完全解耦的组件通信 → 选择发布订阅
  2. 当你处理明确的主从关系 → 选择观察者模式
  3. 性能敏感的简单场景 → 观察者模式更高效
  4. 需要复杂事件处理时 → 发布订阅更灵活

发布订阅和观察者模式就像工具箱中的不同工具,没有绝对的优劣,只有适合的场景。真正优秀的开发者不仅会使用这些模式,更能理解其背后的设计哲学,根据实际需求灵活变通。

下次当你在代码中实现事件通信时,不妨先问自己:我的组件之间是像公众号和订阅者(发布订阅),还是像老师和学生(观察者)?这个简单的思考将帮助你选择最合适的设计模式。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2025-10-13
cookie跨域介绍
Cookie 跨域问题详解 在 Web 开发中, Cookie 是最常见的客户端存储机制之一,用于记录用户登录状态、偏好设置、会话信息等。然而,当涉及到 跨域请求 时,Cookie 的行为往往变得复杂,尤其在现代浏览器的安全策略下,跨域 Cookie 的传递和写入都有严格的限制。 本文将从基础概念开始,逐步讲解 Cookie 的跨域机制、SameSite 属性、CORS 配置及常见问题解决方案。 一、Cookie 的基础概念 Cooki...
学习面试
八股文2025-10-09
JavaScript 闭包详解
JavaScript 闭包(Closure)详解 一、什么是闭包 闭包(Closure) 是 JavaScript 中一个非常核心且常被问到的概念。简单来说: 闭包是一个函数,它可以“记住”并访问其定义时所在的词法作用域,即使这个函数在其作用域之外被调用。 换句话说,当一个函数“嵌套”在另一个函数中,并且 内部函数引用了外部函数的变量 时,就形成了闭包。 示例: 虽然 已经执行完毕,但 依然可以访问 中的 。这就是闭包。 二、闭包的形成...
学习面试
八股文2025-10-07
开始性能优化之旅
事件循环机制 一、JavaScript 引擎的本质 核心职责 : 解析 JavaScript 语法 管理变量和内存 执行代码逻辑 不涉及 : 线程管理(Worker除外) I/O 操作 定时器控制 网络请求 常见引擎:V8(Chrome)、SpiderMonkey(Firefox)、JavaScriptCore(Safari) 二、宿主环境的扩展能力 宿主提供的多线程能力 : | 线程类型 | 功能 | 对应 API | | | | |...
学习面试
八股文2025-09-30
前端首屏优化
话说,我在面试的时候,80%的情况下,都会被问到首屏优化问题,烦,恨 TCP Slow Start(慢启动)概念 咱先不说标题的数字哪里来的,先说一个概念,就是TCP的慢启动。 你们想哈,在浏览器和服务器开始建立连接的时候,服务器是并不知道浏览端网络的带宽、拥塞状况。假设你本地的带宽是1M,如果一开始服务器就发送2M的文件,那浏览器压根就不是人类,忍忍或者挤挤就能接收到的,这样就可能会引发丢包和重传。 想想,这个该有什么办法解决这个问题...
学习面试
八股文2025-09-28
Function.prototype和Object.prototype
一道面试题引发的原型链血案,彻底搞懂 Function.prototype 和 Object.prototype 前言 有这样一道题: 目录 问题分析与答案揭晓 让我们先公布正确答案,然后深入分析原因: 为什么 是 而不是 ? 这是因为函数和实例的原型链路径完全不同! 原型链的本质机制 JavaScript中的两条原型链 在JavaScript中,存在两条不同的原型链: &nbsp; &nbsp; &nbsp; 关键理解:F.proto...
学习面试
八股文2025-09-10
让你彻底明白什么是闭包
今天我们来聊一个听起来很高大上,但实际上你可能天天在用(只是不知道它名字)的概念—— 闭包 。 一、一个你肯定写过的闭包 先别管定义,来看这段代码,你是不是再熟悉不过了? 恭喜你!这就是一个经典的闭包!是不是很简单? 二、为什么会有闭包?—— 背包的故事 想象一下,JavaScript 中的函数就像一个小机器人,当它被创建时,会背着一个神奇的 背包 。 这个背包里装着什么呢?装着它 出生时 所在环境的所有变量! 当我们调用 时,返回的那...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录