手写发布订阅设计模式

前言

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

打个比方:

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

  • 写文章的人就是 发布者 (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不执行 
    
相关推荐
知识分享小能手4 分钟前
Vue3 学习教程,从入门到精通,Vue3 中使用 Axios 进行 Ajax 请求的语法知识点与案例代码(23)
前端·javascript·vue.js·学习·ajax·vue·vue3
533_10 分钟前
[echarts] 更新数据
前端·javascript·echarts
种子q_q14 分钟前
组合索引、覆盖索引、聚集索引、非聚集索引的区别
后端·面试
讨厌吃蛋黄酥17 分钟前
利用Mock实现前后端联调的解决方案
前端·javascript·后端
zzywxc78738 分钟前
在处理大数据列表渲染时,React 虚拟列表是提升性能的关键技术,但在实际实现中常遇到渲染抖动和滚动定位偏移等问题。
前端·javascript·人工智能·深度学习·react.js·重构·ecmascript
前端小巷子1 小时前
Vue 2 Diff 算法
前端·vue.js·面试
_Kayo_7 小时前
VUE2 学习笔记14 nextTick、过渡与动画
javascript·笔记·学习
咔咔一顿操作9 小时前
Vue 3 入门教程7 - 状态管理工具 Pinia
前端·javascript·vue.js·vue3
漂流瓶jz10 小时前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理