业务|手撕发布订阅模式 eventBus

【业务|手撕发布订阅模式 eventBus】什么是发布订阅模式
比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。
1-1. eventBus的基本使用 我们要实现eventBus,首先肯定要知道eventBus它目前具有哪些api
注册EventBus

//main.js import Vue from 'vue'; ... Vue.prototype.EventBus = new Vue();

vm.$on( event, callback )
用法:监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
vm.$emit( eventName, […args] )
用法:触发当前实例上的事件。附加参数都会传给监听器回调。
vm.$once( event, callback )
用法:监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
vm.$off( [event, callback] )
用法:
  • 移除自定义事件监听器。
  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
父组件
import Son from '../components/Son'; export default { components: { Son, }, mounted () { //监听grandson事件 this.EventBus.$on("grandson",this.handleMmsg); }, methods: { handleMmsg(msg) { .... //移除事件监听,监听以后马上又移除了,故Parent只监听一次 this.EventBus.$off("grandson",this.handleMmsg); } }, }

子组件
> import Grandson from '../components/Grandson'; export default { components: { Grandson, }, mounted () { //监听事件 this.EventBus.$on("grandson",(msg)=>{ console.log(msg); }); }, }

孙组件
> export default { methods: { handleMsg() { //发出grandson事件 this.EventBus.$emit("grandson","我是组件GrandSon") } }, }

ok,现在我们来实现吧!!
1-2.实现 实现思路
  • 创建一个对象
  • 在该对象上创建一个缓存列表(调度中心)
  • on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
  • emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
  • off 方法可以根据 event 值取消订阅(取消订阅)
  • once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
注意,on和emit不要搞混了,on才是监听收集事件,emit是发送触发事件,不管有没有emit发布事件,只要有on注册了事件,就会往listeners添加收集(先监听,再发布,发布过程中发现已经监听了就会执行监听回调,因此回调会在emit中执行)。比如说子组件通过emit触发了getName事件,那么所有的监听了这个事件的组件都会执行on方法。
class EventEmitter { constructor() { // 维护事件及监听者 this.listeners = {} }/** * 注册事件监听者 * @param {String} type 事件类型,例如上面写的grandson事件 * @param {Function} cb 回调函数,例如上面写的on接收的grandson对应的函数 * 例如: EventBus.$on("grandson",(msg)=>{console.log(msg); }) */ on(type, cb) { if (!this.listeners[type]) { this.listeners[type] = [] } this.listeners[type].push(cb) }/** * 发布事件 * @param {String} type 事件类型 * @param{...any} args 参数列表,把emit传递的参数赋给回调函数 * 例如: EventBus.$emit("grandson","我是组件GrandSon") */ emit(type, ...args) { if (this.listeners[type]) {// 只要emit一触发,那么所有监听这个事件的on都会接收到,然后执行 this.listeners[type].forEach(cb => { cb(...args) }) } }/** * 移除某个事件的一个监听者 * @param {String} type 事件类型 * @param {Function} cb 回调函数 * 例如:EventBus.$off("grandson",this.handleMmsg); */ off(type, cb) { if (this.listeners[type]) { const targetIndex = this.listeners[type].findIndex(item => item === cb) if (targetIndex !== -1) { this.listeners[type].splice(targetIndex, 1) } if (this.listeners[type].length === 0) { delete this.listeners[type] } } }/** * 移除某个事件的所有监听者 * @param {String} type 事件类型 */ offAll(type) { if (this.listeners[type]) { delete this.listeners[type] } } }// 创建事件管理器实例 const ee = new EventEmitter() // 注册一个chifan事件监听者 ee.on('chifan', function () { console.log('吃饭了,我们走!') }) // 发布事件chifan ee.emit('chifan') // 也可以emit传递参数 ee.on('chifan', function (address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) }) ee.emit('chifan', '三食堂', '铁板饭') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者// 测试移除事件监听 const toBeRemovedListener = function () { console.log('我是一个可以被移除的监听者') } ee.on('testoff', toBeRemovedListener) ee.emit('testoff') ee.off('testoff', toBeRemovedListener) ee.emit('testoff') // 此时事件监听已经被移除,不会再有console.log打印出来了// 测试移除chifan的所有事件监听 ee.offAll('chifan') console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了

特点
  • 发布订阅模式中,对于发布者Publisher和订阅者Subscriber没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。
  • 松散耦合,灵活度高,常用作事件总线
  • 易理解,可类比于DOM事件中的dispatchEventaddEventListener
业务|手撕发布订阅模式 eventBus
文章图片

缺点:
当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。

    推荐阅读