发布订阅模式:实现机制与工程权衡

前言

发布订阅通过事件通道将发布者与订阅者解耦,使系统可在不修改双方代码的前提下横向扩展。本文基于精简 EventEmitter 实现,剖析订阅登记、消息派发及取消订阅的完整数据流,并结合实际场景讨论其松耦合收益与调试、可靠性、资源占用等潜在成本,为架构选型提供参考。


1️⃣ 发布订阅模式的定义

发布订阅模式 是一种设计模式,它定义了一种一对多 的关系,让多个订阅者同时监听某一个发布者,当发布者状态发生改变时,会自动通知所有订阅者。它让发布者订阅者 通过事件通道(Event Channel) 通信,两者完全解耦

  • 一对多:一个发布者可以同时通知任意数量的订阅者;
  • 松耦合:发布者不知道谁在订阅,订阅者也不知道消息是谁发的;
  • 事件驱动 :系统以事件 为核心,按 事件名(eventName) 进行匹配;
  • 异步友好:天然支持同步或异步派发,常用于消息队列、微前端、跨组件通信等场景。

2️⃣ 代码实现

js 复制代码
class EventEmitter {
    // 创建一个纯净的哈希表------事件列表(用于存储事件和回调函数)
    // 事件名作为key,回调函数数组作为value
    constructor() {
        this.eventList = {
            // 'hasHouse': [publish, process],
            // 'hasCar': [done]
        }
    } 

    // 订阅
    on(eventName, callback) {
        // ①判断事件是否存在,不存在则创建
        if(!this.eventList[eventName]) {
            this.eventList[eventName] = []
        }
        // ②将回调函数添加到事件列表中
        this.eventList[eventName].push(callback)
    }

    // 取消订阅
    off(eventName, callback){
        // ①判断事件是否存在,存在则删除对应的回调函数
        if(this.eventList[eventName]) {
            // ②filter()方法过滤出不等于回调函数的回调函数
            this.eventList[eventName] = this.eventList[eventName].filter((item) => {
                return item !== callback
            })
        }
    }

    // 发布
    emit(eventName) {
        // ①判断事件是否存在,存在则调用对应事件的回调函数
        if(this.eventList[eventName]) {
            // ②slice()方法复制事件列表,避免在回调函数中直接操作事件列表
            const handlers = this.eventList[eventName].slice()
            // ③遍历事件列表,调用每个回调函数
            handlers.forEach((item) => {
                item()
            })
        }
    }

    // 一次性订阅
    once(eventName, callback) {
        // ①创建一个包装函数,在调用回调函数后,手动删除包装函数,使包装函数失效
        const wrapper = () => {
            callback()
            this.off(eventName, wrapper)
        }
        // ②将包装函数添加到事件列表中
        this.on(eventName, wrapper)
    }

}

function publish() {console.log('发布事件');}
function process() {console.log('处理事件');}
function done() {console.log('事件处理完成');}

let _event = new EventEmitter() // 事件发射器
_event.on('hasHouse', publish) // 订阅
_event.on('hasHouse', process) // 订阅
_event.off('hasHouse', process) // 取消订阅
_event.once('hasHouse', process) // 一次性订阅
_event.on('hasCar', done) // 订阅

_event.emit('hasHouse') // 发布 => publish,process函数的调用
_event.emit('hasCar') // 发布 => done函数的调用

🧠 记忆口诀

订阅是「存号码」,发布是「拨群号」,取消订阅是「删号码」。

事件通道就是「通讯录」,永远不知道谁打电话,只负责按号码群发。

动作 口诀 关键代码
订阅 存号码 events[type].push(fn)
取消 删号码 events[type]=events[type].filter(...)
发布 群发消息 events[type].forEach(fn=>fn())

3️⃣ 优劣速查表

✅ 五大核心优势

优势 一句话释义 真实场景举例
松耦合 发布者和订阅者互不知晓、可独立迭代 前端 A/B 组件通过事件总线通信,A 重构时无需改动 B
高伸缩性 事件通道可横向扩展 Redis Cluster 支撑上万订阅者实时消息
灵活多对多 同一事件可被 N 个订阅者、N 个发布者共享 微服务日志中心:多个服务同时向一个 Topic 写日志,多个监控实例同时消费
异步非阻塞 发布后立即返回,订阅者在事件循环中并行处理 Node.js EventEmitter 提升 I/O 吞吐量
系统简洁 统一消息格式即可,无需为每个订阅者定制接口 Kafka 只需定义 Avro Schema,上下游按 Schema 读写

❌ 四大典型劣势

劣势 风险表现 实际痛点案例
调试困难 事件链路不透明,断点难以跟踪 前端全局 Bus 滥用 → 数据流"幽灵更新"
消息可靠性 无持久化时,Broker 宕机即丢消息 Redis Pub/Sub 默认不持久化,重启后离线期间消息全部丢失
顺序与重复 并发订阅者可能乱序或重复消费 多个实例并行消费同一分区,需额外幂等设计
资源消耗 订阅数量爆炸 → 内存/CPU 飙升 每订阅一个主题即创建一个队列,未清理会造成内存泄漏
相关推荐
人工智能训练1 小时前
【极速部署】Ubuntu24.04+CUDA13.0 玩转 VLLM 0.15.0:预编译 Wheel 包 GPU 版安装全攻略
运维·前端·人工智能·python·ai编程·cuda·vllm
会跑的葫芦怪1 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js
xiaoqi9222 小时前
React Native鸿蒙跨平台如何进行狗狗领养中心,实现基于唯一标识的事件透传方式是移动端列表开发的通用规范
javascript·react native·react.js·ecmascript·harmonyos
jin1233223 小时前
React Native鸿蒙跨平台剧本杀组队消息与快捷入口组件,包含消息列表展示、快捷入口管理、快捷操作触发和消息详情预览四大核心功能
javascript·react native·react.js·ecmascript·harmonyos
烬头88214 小时前
React Native鸿蒙跨平台实现二维码联系人APP(QRCodeContactApp)
javascript·react native·react.js·ecmascript·harmonyos
pas1364 小时前
40-mini-vue 实现三种联合类型
前端·javascript·vue.js
摇滚侠4 小时前
2 小时快速入门 ES6 基础视频教程
前端·ecmascript·es6
2601_949833394 小时前
flutter_for_openharmony口腔护理app实战+预约管理实现
android·javascript·flutter
珑墨5 小时前
【Turbo】使用介绍
前端
军军君015 小时前
Three.js基础功能学习十三:太阳系实例上
前端·javascript·vue.js·学习·3d·前端框架·three