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

前言

发布订阅通过事件通道将发布者与订阅者解耦,使系统可在不修改双方代码的前提下横向扩展。本文基于精简 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 飙升 每订阅一个主题即创建一个队列,未清理会造成内存泄漏
相关推荐
xjt_090114 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农25 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法