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

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

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

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

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

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

生活中的类比

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

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

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

javascript 复制代码
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 方法时,就是在发布事件。

发布订阅模式的实际应用

javascript 复制代码
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操作例子来理解观察者模式:

html 复制代码
<script>
    let h2 = document.querySelector('h2')
    let btn = document.querySelector('button')
    let obj = {
        count: 1
    }
    let num = obj.count // 防止递归爆栈

    function observer() {
        // 找出页面上所有用到了 count 这个变量的 dom 结构 -- 找到订阅者
        // 将订阅者的内容全部替换为最新的 count 变量
        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响应式系统的核心:

javascript 复制代码
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; // 触发setter,自动更新视图

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

两种模式的核心差异

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

如何选择?实际场景分析

何时选择发布订阅模式

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

    javascript 复制代码
    // 组件A
    eventBus.emit('user-logged-in', userData);
    
    // 组件B
    eventBus.on('user-logged-in', (user) => {
      // 更新用户信息
    });
  2. 微服务架构:不同服务通过消息队列(事件中心)通信

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

何时选择观察者模式

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

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

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

    javascript 复制代码
    button.addEventListener('click', handler); // 主题(button)直接通知观察者(handler)

性能与复杂度权衡

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

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

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

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

常见误区澄清

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

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

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

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

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

javascript 复制代码
// 创建事件中心
const EventBus = new Vue();

// 组件A(发布者)
EventBus.$emit('data-updated', payload);

// 组件B(订阅者)
EventBus.$on('data-updated', handleData);

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

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

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

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

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

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

思考题:在React的Context API中,Provider和Consumer之间的关系更接近哪种模式?为什么?欢迎在评论区分享你的见解!

相关推荐
WebInfra几秒前
深度剖析 tree shaking:主流打包工具的实现对比
前端·javascript·架构
weixin_471525784 分钟前
【学习嵌入式day-17-数据结构-单向链表/双向链表】
前端·javascript·html
jingling55516 分钟前
Git 常用命令指南:从入门到高效开发
前端·javascript·git·前端框架
索西引擎18 分钟前
【前端】网站favicon图标制作
前端
程序员海军24 分钟前
告别低质量Prompt!:字节跳动PromptPilot深度测评
前端·后端·aigc
华洛26 分钟前
关于可以控制大模型提升任意产品的排名这件事📈
前端·github·产品经理
Yanc27 分钟前
翻了vue源码 终于解决了这个在SFC中使用tsx的bug
前端·vue.js
nujnewnehc31 分钟前
失业落伍前端, 尝试了一个月 ai 协助编程的真实感受
前端·ai编程·github copilot
大熊学员33 分钟前
HTML 媒体元素概述
前端·html·媒体
萌萌哒草头将军34 分钟前
VoidZero 发布消息称 Vite 纪录片即将首映!🎉🎉🎉
javascript·vue.js·vite