持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
EventBus
是事件总线的意思,可不是什么事件车。 事件总线模式在工作中经常使用,在面试中也很容易问到。甚至在很多面试中会让你手写一个EventBus
,那么EventBus
到底是个什么东西,今天我们就来学一学!
1.什么是EventBus
?
所谓事件总线模式,其实就和发布订阅模式非常类似,比如我们订阅了一个公众号,公众号发布文章之后我们就能收到信息,这就是一种订阅发布的关系。
再比如在Vue
项目中,我们可以使用$on
、$emit
来实现事件的监听和触发,这其实就是一种事件总线的思想在里面,只不过Vue
帮我们实现好了。
在我们的JavaScript
中,可以给元素添加一个点击监听事件,当用户点击的时候,点击事件怎会被执行,这也是一种事件总线的思想在里面,就好比元素订阅了点击事件,用户发布或出触发点击事件。
从上可以看出,事件总线模式在我们的开发中经常出现,我们也可以通过一张图来更加清楚的认识什么是事件总线。
上图很清晰的描述了EventBus
的角色是什么,它主要承担的是一个发布订阅的责任,比如有人发布了信息出来,那其他人如何接收这些信息呢?其他人可以通过EventBus
来订阅这些信息,当有信息发布的时候,事件总线就将这些信息传播给订阅者。
2.乞丐版EventBus
既然我们了解了EventBus
的原理后,我们便可以手动实现一个来。我们步子不能迈的太大,先来一个乞丐版,实现嘴贱的发布订阅功能。
实现目标:
- 使用
$on
订阅事件 - 使用
$emit
发布事件
代码示例:
<script>
class EventBus {
// 定义所有事件列表,格式如下:
// {
// key: Array,
// key: Array,
// }
// Array存储的是注册的回调函数
constructor() {
this.eventObj = {}; // 用于存储所有订阅事件
}
// 订阅事件,类似监听事件$on('key',()=>{})
$on(name, callbcak) {
// 判断是否存储过
if(!this.eventObj[name]) {
this.eventObj[name] = [];
}
this.eventObj[name].push(callbcak); // 往事件数组里面push
}
// 发布事件,类似于触发事件$emit('key')
$emit(name) {
// 获取存储的事件回调函数数组
const eventList = this.eventObj[name];
// 执行所有回调函数
for (const callbcak of eventList) {
callbcak();
}
}
}
// 初始化EventBus
let EB = new EventBus();
// 订阅事件
EB.$on('key1', () => {
console.info("我是订阅事件A");
})
EB.$on("key1", () => {
console.info("我是订阅事件B");
})
EB.$on("key2", () => {
console.info("我是订阅事件C");
})
// 发布事件
EB.$emit('key1');
EB.$emit('key2');
</script>
复制代码
输出结果:
上段代码中我们声明了一个EventBus
类,专门用来处理我们的发布订阅操作。上段代码的整体思路如下:
- 首先订阅了一堆事件
key1
、key2
等等,当这些事件被触发时执行回调函数。 - 订阅的这些事件
key1...
都需要存储到EventBus
中去,定义变量eventObj
存储。 - 当其它用户或模块触发订阅的事件
key1...
等,EventBus
就去eventObj
中查找,找到则则触发存储的回调函数。
上面就是一个最简单的EventBus
了,只初步实现了$on
和$emit
。
3.传参版EventBus
虽然说乞丐版的EventBus
也能用,但是它的场景很有限。我们使用EventBus
是很多时候都是需要传参的,就好比我订阅了一个公众号,结果公众号每次发布的内容都是空的,很明显不行。所以我们需要在发布事件$emit
的时候传参,然后在订阅事件的回调函数里面可以接收参数。
实现目标:
- 使用
$on
订阅事件 - 使用
$emit
发布事件 $emit
发布事件可以传参
示例代码:
<script>
class EventBus {
// 定义所有事件列表,格式如下:
// {
// key: Array,
// key: Array,
// }
// Array存储的是注册的回调函数
constructor() {
this.eventObj = {}; // 用于存储所有订阅事件
}
// 订阅事件,类似监听事件$on('key',()=>{})
$on(name, callbcak) {
// 判断是否存储过
if (!this.eventObj[name]) {
this.eventObj[name] = [];
}
this.eventObj[name].push(callbcak); // 往事件数组里面push
}
// 发布事件,类似于触发事件$emit('key')
$emit(name, ...args) {
// 获取存储的事件回调函数数组
const eventList = this.eventObj[name];
// 执行所有回调函数且传入参数
for (const callbcak of eventList) {
callbcak(...args);
}
}
}
// 初始化EventBus
let EB = new EventBus();
// 订阅事件
EB.$on('key1', (name, age) => {
console.info("我是订阅事件A:", name, age);
})
EB.$on("key1", (name, age) => {
console.info("我是订阅事件B:", name, age);
})
EB.$on("key2", (name) => {
console.info("我是订阅事件C:", name);
})
// 发布事件
EB.$emit('key1', "小猪课堂", 26);
EB.$emit('key2', "小猪课堂");
</script>
复制代码
输出结果:
上段代码我们实现了$emit
传参,$on
的回调可以接收参数的功能。这样我们的EventBus
又变得完善了一点。比如有两个标签页需要通信,就可以使用我们这里的EventBus
来进行实现。除此之外,Vue
组件间的通讯也可以使用EventBus
来进行实现,因为可以传递参数。
4.取消订阅版EventBus
既然有订阅,那么就有取消订阅,我们可以订阅公众号,也可以取关公众号,这是理所当然的事。我们的EventBus
也需要实现这样的功能,比如我们监听了某个事件,在一定情况下我们需要取消监听事件。
实现目标:
- 使用
$on
订阅事件 - 使用
$emit
发布事件 $emit
发布事件可以传参- 实现
$off
取消订阅
示例代码:
<script>
class EventBus {
// 定义所有事件列表,此时需要修改格式:
// // {
// key: {
// id: Function,
// id: Function
// },
// key: Object,
// }
// Array存储的是注册的回调函数
constructor() {
this.eventObj = {}; // 用于存储所有订阅事件
this.callbcakId = 0; // 每个函数的ID
}
// 订阅事件,类似监听事件$on('key',()=>{})
$on(name, callbcak) {
// 判断是否存储过
if (!this.eventObj[name]) {
this.eventObj[name] = {};
}
// 定义当前回调函数id
const id = this.callbcakId++;
this.eventObj[name][id] = callbcak; // 以键值对的形式存储回调函数
return id; // 将id返回出去,可以利用该id取消订阅
}
// 发布事件,类似于触发事件$emit('key')
$emit(name, ...args) {
// 获取存储的事件回调函数数组
const eventList = this.eventObj[name];
// 执行所有回调函数且传入参数
for (const id in eventList) {
eventList[id](...args);
}
}
// 取消订阅函数,类似于$off('key1', id)
$off(name, id) {
// 删除存储在事件列表中的该事件
delete this.eventObj[name][id];
console.info(`id为${id}的事件已被取消订阅`)
// 如果这是最后一个订阅者,则删除整个对象
if (!Object.keys(this.eventObj[name]).length) {
delete this.eventObj[name];
}
}
}
// 初始化EventBus
let EB = new EventBus();
// 订阅事件
EB.$on('key1', (name, age) => {
console.info("我是订阅事件A:", name, age);
})
let id = EB.$on("key1", (name, age) => {
console.info("我是订阅事件B:", name, age);
})
EB.$on("key2", (name) => {
console.info("我是订阅事件C:", name);
})
// 发布事件key1
EB.$emit('key1', "小猪课堂", 26);
// 取消订阅事件
EB.$off('key1', id);
// 发布事件key1
EB.$emit('key1', "小猪课堂", 26);
// 发布事件
EB.$emit('key2', "小猪课堂");
</script>
复制代码
输出结果:
既然我们需要取消订阅某一个事件,那么我们就需要给该事件添加一个标识,这样才可以在茫茫人海中找到它,并且取阅它。所以我们这个修改了eventObj
事件对象的存储结构,其中的每个key
存储的不在是数组了,而是对象类型。
实现的整体思路如下:
- 给每个订阅事件添加唯一标识
id
。 - 之前事件
key
存储的是事件回调函数数组,现在改成事件回调函数对象,键为id
,值为回调函数。 - 订阅事件的时候返回一个
id
。 - 取消订阅的时候通过
id
找到存储在eventObj
中的事件函数,并且删掉它。
5.执行一次版EventBus
(完整版)
虽然前面实现的EventBus
基本能够满足我们的项目需求了,但是还有一种情景我们需要考虑,比如我订阅了某个公众号,但是我只允许你给我发送一次消息,然后我们取关你。虽然在公众号的场景下这种需求比较变态,但是在我们的项目场景下这可能就是比较正常的了,我只想让某一个订阅事件只执行一次,这非常正常。
- 使用
$on
订阅事件 - 使用
$emit
发布事件 $emit
发布事件可以传参- 实现
$off
取消订阅 - 实现
$once
执行一次
示例代码:
<script>
class EventBus {
// 定义所有事件列表,此时需要修改格式:
// // {
// key: {
// D+id: Function,
// id: Function
// },
// key: Object,
// }
// Array存储的是注册的回调函数
constructor() {
this.eventObj = {}; // 用于存储所有订阅事件
this.callbcakId = 0; // 每个函数的ID
}
// 订阅事件,类似监听事件$on('key',()=>{})
$on(name, callbcak) {
// 判断是否存储过
if (!this.eventObj[name]) {
this.eventObj[name] = {};
}
// 定义当前回调函数id
const id = this.callbcakId++;
this.eventObj[name][id] = callbcak; // 以键值对的形式存储回调函数
return id; // 将id返回出去,可以利用该id取消订阅
}
// 发布事件,类似于触发事件$emit('key')
$emit(name, ...args) {
// 获取存储的事件回调函数数组
const eventList = this.eventObj[name];
// 执行所有回调函数且传入参数
for (const id in eventList) {
eventList[id](...args);
// 如果是订阅一次,则删除
if(id.indexOf('D') !== -1) {
delete eventList[id];
}
}
}
// 取消订阅函数,类似于$off('key1', id)
$off(name, id) {
console.log(this.eventObj)
// 删除存储在事件列表中的该事件
delete this.eventObj[name][id];
console.info(`${id}id事件已被取消订阅`)
// 如果这是最后一个订阅者,则删除整个对象
if (!Object.keys(this.eventObj[name]).length) {
delete this.eventObj[name];
}
}
// 订阅事件,只会执行一次,为了方便,id上直接加上一个标识d
$once(name, callbcak){
// 判断是否存储过
if (!this.eventObj[name]) {
this.eventObj[name] = {};
}
// 定义当前回调函数id,添加D则代表只执行一次
const id = "D" + this.callbcakId++;
this.eventObj[name][id] = callbcak; // 以键值对的形式存储回调函数
return id; // 将id返回出去,可以利用该id取消订阅
}
}
// 初始化EventBus
let EB = new EventBus();
// 订阅事件
EB.$on('key1', (name, age) => {
console.info("我是订阅事件A:", name, age);
})
EB.$once("key1", (name, age) => {
console.info("我是订阅事件B:", name, age);
})
EB.$on("key2", (name) => {
console.info("我是订阅事件C:", name);
})
// 发布事件key1
EB.$emit('key1', "小猪课堂", 26);
console.info("在触发一次key1")
EB.$emit('key1', "小猪课堂", 26);
// 发布事件
EB.$emit('key2', "小猪课堂");
</script>
复制代码
输出结果:
上段代码中我们第一次触发事件key1
时,订阅事件A
和B
都执行了,但是第二次触发事件key1
时,只有订阅事件A
执行了,说明订阅事件B
已经取消订阅了。
整体实现思路:
- 使用
$once
订阅事件时,我们需要给该事件做个标识,以便识别出它只会执行一次。 - 为了方便,我们直接在
id
字段前面拼接D
代表只会执行一次。 - 触发事件时,判断
id
上是否有D
,如果有D
,则执行完后删除该事件。
总结
本篇文章实现了一个较为简单的EventBus
,但是也基本满足我们在项目中的使用了。想要实现EventBus
,我们首要理解的就是发布订阅模式,然后我们在思考下面几个问题,基本上就能实现一个属于自己的EventBus
了。
- 订阅事件如何存储?
- 如何传递参数?
- 如何给每个订阅事件添加唯一标识?
- 如何正确删除存储的订阅事件?
$on
和$emit
主要担任的是什么角色?- 什么时发布订阅模式?
想明白了上面的问题那么EventBus
我相信你也明白了,能够自己实现了。
如果觉得文章太繁琐或者没看懂,可以观看视频: 小猪课堂