文章目录
- 什么是自定义事件总线
- 具体实现
- 思路分析
- 定义结构
- 实现 on
- 实现 emit
- 实现 off
- 源码
什么是自定义事件总线
- 自定义事件总线属于一种观察着模式,其中包括三个角色
- 发布者(Publisher):发出事件(Event)
- 订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler)
- 事件总线(EvnetBus):无论是发布者还是订阅者都是通过事件总线作为中台的
具体实现
思路分析
-
事件总线相信大家都用过或者听过,特别是在 vue2 中,这就是一种组件传值的方式,轻量又简单
-
从使用上来说,通过 on 监听事件,同时可以使用 emit 来发送事件,当然也可也通过 off 来取消事件
-
使用还是非常简单的,但是怎么才能实现 emit 触发 on 监听的呢,竟然要触发的话,首先我是不是需要有一个地方可以存储这个通过 on 监听的方法呢?存储了之后,就是使用,emit 实际就是通过参数找到对应的存储方法,然后我们帮他手动调用一下
-
所以基于这一点,我们就可以得到三个点,on、store、emit,如图:
定义结构
-
根据上面的分析,我们可以选用 class 的形式来实现,
-
其次我们还需要 on emit off 三个方法,如下:
class JcEventBus{ constructor() { } on(){} emit(){} off(){} }
-
在这之中,我们就按照分析,确定一下 store,在 class 结构中,可以 this 访问数据和方法,所以在 JcEventBus 初始化时,就可以定义一个变量来存储后续的方法,而可以通过一个字符串准确存储和取出的话,对象和map都可以,但是这里对象会更加好操作,所以改造后 constructor 代码如下:
constructor() { this.eventBus = {} }
实现 on
-
on 方法一般是有两个参数的,我们可以写出基础的函数结构,如下:
on(eventName, callback){}
-
我们不妨在思考一下,有时候这个回调函数的this,可能需要我们来指定,方便在函数内部使用 this,所以为了实现这一点,一般会给 callback 传递一个普通函数而非匿名函数,同时应该有第三个参数来接受这个 this,如下:
on(eventName, callback, ctx){}
-
确定好参数之后,我们来进行一下具体的实现,首先假设监听的名称为 foo,而且监听的函数有时候可能不止一个,而存储多个函数我们可以想到什么,数组,利用数组来实现存储这些函数,需要用的时候在找到对应的函数进行执行即可,确定好这一点之后,我们要做的就是以 eventName 为 key,callback 存储在数组中为 value,{ foo: [fn1,fn2…] },代码如下:
on(eventName, callback, ctx){ // 如果不存在则初始化为空数组 const handles = this.eventBus[eventName] || [] // 传递一个对象,同时保存 执行的函数 和 this handles.push({ callback, ctx }) // 存储在 store 中 this.eventBus[eventName] = handles }
-
我们来通过实例化 JcEventBus 来进行测试一下,看看是否真的存入了,测试代码如下:
eventBus.on( 'foo', function (...payload) { console.log('foo 函数,参数为:', payload) }, obj ) eventBus.on( 'bar', function (...payload) { console.log('bar 函数,参数为:', payload) }, obj ) console.log(eventBus.eventBus)
-
输出如图:
实现 emit
-
emit 要做的事件也非常简单,传递事件名称和参数即可,因此基础的函数结构如下:
emit(eventName, ...payload){}
-
参数可能会有多个,使用剩余参数来接收,这个方法,第一步就是要查找有没有这个方法,如果有,就执行这个事件名称上绑定的所有函数,代码如下:
emit(eventName, ...payload) { const handles = this.eventBus[eventName] if (!handles || !handles.length) return handles.forEach(handle => { // 使用 apply 绑定 this handle.callback.apply(handle.ctx, payload) }) }
-
这个应该非常简单吧,直接上测试代码。如下:
const eventBus = new JcEventBus() const obj = { name: 'coderjc' } eventBus.on( 'foo', function (...payload) { console.log('foo1 函数,参数为:', payload, this) }, obj ) eventBus.on( 'foo', function (...payload) { console.log('foo2 函数,参数为:', payload) }, obj ) eventBus.emit('foo', 1, 2, 3)
-
结果如图:
实现 off
-
off 类似于 removeEventListener 这个方法,也是事件名 + 函数,即可取消,所以函数基础结构如下:
off(eventName, callback){}
-
然后就是通过事件名称找到这个数组,在这个数组里面找到对应的函数进行删除即可,不过这里会会有一点细节需要注意,比如一个函数被存储了多次的情况下,当然这个你可以在存储的时候就拦截,避免重复,但是我们这里没有,所以如果删除就要删除多个,而这个删除可能有部分的朋友们就会想到遍历,全等判断,然后删除就行吗,这个也可以,但是可能需要多一点的操作,我们先看看这个思路删除会有什么结果,我写了一个demo,如下:
// 这里用数字代替函数 const arr = [1, 2, 2, 4, 5, 6] const val = 2 for (let i = 0; i < arr.length; i++) { if (arr[i] === val) { // 删除 arr.splice(i, 1) } } console.log(arr)
-
结果如图:
-
不知道发现了没有,只删除了其中的一个2,还有一个没有删除,这是因为 splice 方法删除之后,原数组的长度就-1了,此时原来索引为2的数组就会变成索引为1,而 i 的值又没有同步的 -1,就会直接跳过,所以就是这个结果了所以解决方法很简单,同时 i-1 即可,当然还是有其他方法的,都是非常简单的
-
我这里就直接过滤数组重新赋值了,如下:
off(eventName, callback){ const handles = this.eventBus[eventName] if (!handles || !handles.length) return this.eventBus[eventName] = handles.filter(handle => handle.callback !== callback) }
-
来看看是不是真的有用,测试代码如下:
const eventBus = new JcEventBus() function foo(...payload) { console.log('foo1 函数,参数为:', payload) } eventBus.on('foo', foo) eventBus.on('foo', function (...payload) { console.log('foo2 函数,参数为:', payload) }) // 移除 eventBus.off('foo', foo) eventBus.emit('foo', 1, 2, 3)
-
结果如图:
源码
当然了,最后我是加了一些对于参数的类型判断,非常简单,就不单独介绍了
function _verifyType(eventName = null, callback = null) {
if (eventName && typeof eventName !== 'string') {
throw new Error('eventName must be a string')
}
if (callback && typeof callback !== 'function') {
throw new Error('callback must be a function')
}
}
class JcEventBus {
constructor() {
this.eventBus = {}
}
on(eventName, callback, ctx) {
_verifyType(eventName, callback)
const handles = this.eventBus[eventName] || []
handles.push({ callback, ctx })
this.eventBus[eventName] = handles
}
off(eventName, callback) {
_verifyType(eventName, callback)
const handles = this.eventBus[eventName]
if (!handles || !handles.length) return
this.eventBus[eventName] = handles.filter(handle => handle.callback !== callback)
}
emit(eventName, ...payload) {
_verifyType(eventName)
const handles = this.eventBus[eventName]
if (!handles || !handles.length) return
handles.forEach(handle => {
handle.callback.apply(handle.ctx, payload)
})
}
}