手写发布订阅设计模式

前言

什么是发布订阅设计模式?

打个比方:

  • 想象一个 微信公众号平台

  • 写文章的人就是 发布者 (Publisher)

  • 看文章的人就是 订阅者 (Subscriber)

  • 微信公众号平台本身就是 事件通道/消息代理/事件总线 (Event Channel/Message Broker/Event Bus)

  • 作者写好文章后,不是直接发给读者,而是 发布 (Publish) 到公众号平台上,说"我写了一篇关于'设计模式'的文章"。

  • 读者对"设计模式"感兴趣,就 订阅 (Subscribe) 了这个主题(Topic)。

  • 平台负责 把消息从发布者传递给订阅者。当有新的"设计模式"文章发布时,平台会自动推送给所有订阅了该主题的读者。

  • 关键点:作者 不知道 谁订阅了他的文章;读者 也不知道 文章具体是谁写的(除非作者署名)。他们只和平台打交道。

简单来说:读者看到自己喜欢的文章,则会点击订阅,平台收到后将订阅消息先存储起来,等到发布者发布了该类文章,则平台找到喜欢该文章的订阅消息,将其执行,也就是向读者推荐该文章。

那么要如何实现发布订阅设计模式呢?

底层逻辑:定义一个EventEmitter 类,该类内部定义了订阅函数on发布函数emit 以及enventList对象 (用于存储订阅消息的),再创建一个EventEmitter类的**_event实例**(事件总线),每当有订阅信息,_event则调用on方法将回调函数(订阅者)与特定事件名称关联起来,存储在内部的 eventList 对象中。只要等到发布文章时,_event调用emit方法查找对应事件的所有回调函数并执行,实现了发布者与订阅者的解耦 。

js 复制代码
class EventEmitter{
    constructor(){
        //定义eventList对象,用于存储各类文章的订阅事件
        this.eventList={}
    }
    //订阅函数,eventName未订阅的书籍类型,cb订阅者的回调函数,发布时调用
    on(eventName,cb){//订阅事件
        //当对象内不存在该类订阅书籍,则添加一个新数组进行存储
        if(!this.eventList[eventName]){
            this.eventList[eventName]=[]
        }
        //将书籍类型设为键,订阅者的回调函数加入到该类的数组中
        this.eventList[eventName].push(cb)
    }
    //发布函数,当发布者发布文章时,事件总线则调用该函数,执行所有该类文章的回调函数
    //eventName表示要发布的文章的类别
    emit(eventName){//发布事件
        //查找订阅消息对象中有无该类文章的订阅消息
        if(this.eventList[enventName]){
        //将订阅的事件回调函数复制一份,避免直接操作订阅的事件回调函数数组
            const handlers=this.eventList[eventName].slice()
            //遍历订阅的事件回调函数数组,执行每个回调函数
            handlers.forEach(item=>{
                item()
            })
        }
    }
}
//创建事件总线_event
let _event=new EventEmitter()
//订阅者kang,
function kang(){
    console.log('小康收到文学类文章')
}
//订阅者liu
function liu(){
    console.log('小刘收到艺术类文章')
}
//订阅者cheng
function cheng(){
    console.log('小成收到科学类文章')
}
_event.on('wenxue',kang)//订阅事件'wenxue',订阅者kang
_event.on('kexue',cheng)//订阅事件'kexue',订阅者cheng
_event.emit('wenxue')//发布事件'wenxue'-->订阅者kang执行 
_event.emit('kexue')//发布事件'hasCar'-->订阅者cheng执行 

简单总结:事件总线调用on方法将小康和小刘的订阅消息分别存储到文学和艺术类订阅数组内,当发布者发布了文学类书籍时,事件总线则会调用emit方法,将该书籍推荐给小康,小康则会收到文学类书籍,但小刘收不到,小刘只有当艺术类书籍发布时才会收到。

如何实现取消订阅呢?

大体思路 :就是在EventEmitter类内再添加一个off方法用于取消订阅,当有读者想取消订阅时,事件总线则调用off方法,将该读者的订阅消息从其订阅的那类书籍的数组内删除即可

js 复制代码
//eventName为要取消哪类书籍的订阅,cb为订阅者的回调函数
off(eventName,cb){//取消订阅事件
    //查找该读者是否订阅过该类书籍
    if(!this.eventList[eventName].find(item=>item===cb)){
        return 
    }
    const handlers=this.eventList[eventName]
    //将订阅的事件回调函数数组中,与取消订阅的事件回调函数不同的回调函数保留,相同的回调函数删除
    this.enventList[eventName]=handlers.filter(item=>item!==cb)
}
_event.off('wenxue',kang)//表示订阅者kang取消了文学类书籍的订阅事件

如何实现单次订阅呢?

大体思路:

  1. 创建包装函数wrap(形成闭包环境)
  • 闭包捕获:原始回调cb、事件名称eventName、当前this上下文
  1. wrap函数内部逻辑:
  • 执行原始回调cb()
  • 调用this.off(eventName, wrap)移除自身
  1. 使用this.on(eventName, wrap)将wrap函数添加到订阅消息数组内

当事件触发时:

  • 执行wrap函数

  • 先执行原始回调

  • 随后移除wrap自身

  • 实现只触发一次的效果

js 复制代码
    once(eventName,cb){//单次订阅
        //创建wrap函数,具有闭包特性,wrap函数会记住当前的cb函数以及eventName和this值,当其被调用时引用
        const wrap=()=>{
            cb()
            this.off(eventName,wrap)
        }
        //将wrap添加到订阅消息内,发布时调用
        this.on(eventName,wrap)
    }
    _event.once('yishu',liu)//单次订阅事件'yishu',订阅者liu
    _event.emit('yishu')//发布事件'yishu'-->订阅者liu执行 
    _event.emit('yishu')//发布事件'yishu'-->订阅者liu不执行 
    
相关推荐
折果30 分钟前
如何在vue项目中封装自己的全局message组件?一步教会你!
前端·面试
汪子熙33 分钟前
Vite 极速时代的构建范式
前端·javascript
叶常落34 分钟前
[react] js容易混淆的两种导出方式2025-08-22
javascript
前端小巷子1 小时前
Vue3的渲染秘密:从同步批处理到异步微任务
前端·vue.js·面试
牛奶咖啡132 小时前
学习设计模式《二十三》——桥接模式
学习·设计模式·桥接模式·认识桥接模式·桥接模式的优点·何时选用桥接模式·桥接模式的使用示例
GHOME3 小时前
Vue2知识点详细回顾(以及自己的一些思考和解答)-2
前端·vue.js·面试
摸着石头过河的石头3 小时前
大模型时代的前端开发新趋势
前端·javascript·ai编程
PineappleCoder3 小时前
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
前端·面试·canvas
左灯右行的爱情3 小时前
深度学习设计模式:责任链(Chain of Responsibility)模式(例子+业务场景+八股)
深度学习·设计模式·责任链模式
洋流3 小时前
0基础进大厂,第22天 : CSS中的定位布局,你的.container还找不到位置吗?
前端·javascript·面试