什么是发布订阅模式
比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。
我们要实现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);
}
},
}
</script>
子组件
<template>
<div>
<hr>
我是子组件
<br>
<Grandson></Grandson>
</div>
</template>
<script>
import Grandson from '../components/Grandson';
export default {
components: {
Grandson,
},
mounted () {
//监听事件
this.EventBus.$on("grandson",(msg)=>{
console.log(msg);
});
},
}
</script>
孙组件
<template>
<div>
<hr>
我是孙子组件
<br>
<button @click="handleMsg">点击发出消息</button>
</div>
</template>
<script>
export default {
methods: {
handleMsg() {
//发出grandson事件
this.EventBus.$emit("grandson","我是组件GrandSon")
}
},
}
</script>
ok,现在我们来实现吧!!
实现思路
注意,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
事件中的dispatchEvent
和addEventListener
。
缺点:
当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。