EventBus,事件总线。总线一词来自于《计算机组成原理》中的”系统总线“,是指用于连接多个部件的信息传输线,各部件共享的传输介质。我们通常把事件总线也成为自定义事件,一般包含on
、once
、emit
、off
等方法。在Vue2中想要实现EventBus比较简单,直接暴露出一个new Vue()
实例即可,以此为思路,我们应该如何自定义实现EventBus呢?
EventBus有什么功能
我们如果想要实现一个自定义的EventBus,那么首先就需要明白需要实现什么功能:
- on(eventKey, callback):新增事件监听器,可以一个eventKey绑定多个callback,使用emit来触发指定eventKey的所有callback
- once(eventKey, callback):监听一个自定义事件,但是只会执行一次,执行完毕后当前事件监听器会被移除
off([eventKey, callback]):参数eventKey和callback都是可选的,
- 如果没有提供参数,则移除所有的事件监听器
- 如果只提供了eventKey,则移除该事件所有的监听器
- 如果同时提供了eventKey和callback,则只移除这个回调的监听器
- emit(eventKey, [...args]):触发指定的事件,附加参数都会传给监听器回调。如果执行的是once定义的监听器,则执行后将会移除该监听器
如果要实现这些功能,那么我们应该怎么入手呢?
怎么实现EventBus
知道了EventBus是什么,那么接下来就分析一下应该怎么实现:
- on和once是用来注册函数的,并将其保存到数组中,因为要维持插入顺序和执行顺序一致
- emit根据key值找到存放回调函数的数组,并执行数组里面的所有函数,可以传入额外的参数
- off则根据传入的参数,也可能不传参数,找到函数并删除
实现代码
class EventBus {
/**
* {
* key1: [
* {fn: fn1, isOnce: false}.
* {fn: fn2, isOnce: false}
* {fn: fn3, isOnce: true}
* ],
* key2: [], // 数组可保证注册函数的顺序和执行顺序一致
* key3: [],
* }
*/
constructor() {
this.events = {}; // 初始值为空对象
}
on(eventKey, fn, isOnce= false) {
const events = this.events; // 引用赋值
if (events[eventKey] == null) {
events[eventKey] = []; // 初始化eventKey对应的fn数组
}
// 将函数添加到数组中
events[eventKey].push({fn, isOnce});
}
once(eventKey, fn) {
// 代码复用
this.on(eventKey, fn, true);
}
off(eventKey, fn) {
// 如果传入了函数,但是未指定eveneky,直接不执行
if (!eventKey && fn) return;
if (!eventKey && !fn) {
// 如果未传入参数,则清除所有绑定的函数
this.events = {};
} else if (eventKey && !fn) {
// 解绑当前eventKey对应的函数
this.events[eventKey] = [];
} else {
// 解绑eventKey和fn对应的函数
if (this.events[eventKey]) {
this.events[eventKey] = this.events[eventKey].filter(item => item.fn !== fn);
}
}
}
emit(eventKey, ...args) {
const fnList = this.events[eventKey]; // 引用赋值
if (fnList == null) return;
this.events[eventKey] = fnList.filter(item => {
const {fn, isOnce} = item;
fn(...args); // 执行函数,并传入额外参数
if (!isOnce) return true; // 如果不是once,表示后续还可以继续被执行
return false; // 如果是once,表示执行一次后就要被过滤掉
})
}
}
测试案例
const e = new EventBus();
function fn1(a, b) {
console.log('fn1', a , b);
}
function fn2(a, b) {
console.log('fn2', a , b);
}
function fn3(a, b) {
console.log('fn3', a , b);
}
function fn4(a, b) {
console.log('fn4', a , b);
}
e.on('key1', fn1);
e.on('key1', fn2);
e.once('key1', fn3);
e.emit('key1', 10, 20);
e.off('key1', fn1);
e.emit('key1', 30, 40);
执行结果
过程分析
- EventBus在key1对应的数组中注册三个函数,其中fn3为仅执行一次
- 执行key1对应的数组中的所有函数,执行完毕后,此时key1的对应的函数数组只有fn1和fn2
- 指定删除key1对应的数组中fn1函数,此时key1的对应的函数数组只有fn2
- 执行key1对应的数组中的所有函数,也就是执行fn2
单例模式的EventBus
一个简单的EventBus虽然实现了,但是如果放在实际使用还是有问题的:多次实例化EventBus,其注册的函数不会被共享。还是上面的例子,我们新增测试案例;
function fn4(a, b) {
console.log('fn4', a , b);
}
e2.on('key1', fn4);
e.emit('key1', 30, 40); // fn4函数不会被执行
究其原因,我们多次实例创建了多个对象,每个对象注册的函数都是独立的,因此我们就需要引入单例模式了,保证多次实例化仍然对应的是同一个实例。
简单改造一下构造函数:
class EventBus {
static instance; // 定义一个静态属性,用于实现单例模式
constructor() {
// 如果是第一次实例化,则返回初始对象
if (!EventBus.instance) {
EventBus.instance = {};
this.events = EventBus.instance;
return;
}
// 否则,直接返回类的静态属性,避免重复实例化
this.events = EventBus.instance;
}
...
}
我们再来执行测试案例:
结果就是我们所预期的。
总结
本文提供了一种实现EventBus的方法,其实也并不复杂,需要注意几点:
- 使用数组保证函数有序,使用对象便于获取
- 由于存在once,需要在函数执行后将其从函数数组中过滤掉
- 需要实现单例模式,保证全局只有一个EventBus实例