前言
事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
我们来看看事件总线的处理流程:
看到这里我知道肯定有些小伙伴还是有点蒙吧(其实我是蒙的),没关系,我们一步一步来,不急
事件的本质
我们先来探讨一下事件的概念。
开发过WinForm程序的都知道,我们在做UI设计的时候,从工具箱拖入一个注册按钮(btnRegister),双击它,VS就会自动帮我们生成如下代码:
void btnRegister_Click(object sender, EventArgs e)
{
// 事件的处理
}
其中object sender指代发出事件的对象,这里也就是button对象;EventArgs e 事件参数,可以理解为对事件的描述 ,它们可以统称为事件源。其中的代码逻辑,就是对事件的处理。我们可以统称为事件处理。
说了这么多,无非是想透过现象看本质:事件是由事件源触发并由事件处理消费(An event is raised by an event source and consumed by an event handler)。
好了,事件的本质了解清楚了,我们来看一下发布订阅模式吧!
发布订阅模式
定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。 ——发布订阅模式
发布订阅模式主要有两个角色:
- 发布方(Publisher):也称为被观察者,当状态改变时负责通知所有订阅者。
- 订阅方(Subscriber):也称为观察者,订阅事件并对接收到的事件进行处理。
发布订阅模式有两种实现方式:
- 简单的实现方式:由Publisher维护一个订阅者列表,当状态改变时循环遍历列表通知订阅者。
- 委托的实现方式:由Publisher定义事件委托,Subscriber实现委托。 总的来说,发布订阅模式中有两个关键字,通知和更新。 被观察者状态改变通知观察者做出相应更新。 解决的是当对象改变时需要通知其他对象做出相应改变的问题。
口述态复杂了,我们还是来看图吧!
好啦好啦,前菜上完了,该上正餐了。
事件总线(发布订阅模式)
class EventEmitter {
constructor() { //创建一个数据源
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'
这么多看下来,小伙伴是不是很绝望啊,没事没事,我们解析一下就好啦!
- 创建一个仓库
constructor() { //创建一个数据源
this.cache = {}
}
- 绑定事件:
on(name, fn) {}
第一步判断当前事件是否存在,如果存在 ,就将fn push到数据中即可。否则 就初始化 使cache[name]:[fn]
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
- 解绑事件:
off(name, fn) {}
第一步判断当前事件tasks是否存在,再判断第二个参数是否存在,如果存在,就把fn或者callback移除
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
- 触发事件:
emit(name, once = false, ...args) {}
第一步判断当前事件是否存在 如果存在 遍历数组中的索引 调用函数即可 如果name存在 将paramsc传递函数中
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
// ...args意思可以拿到除开始参数外的参数,即剩余参数
}
if (once) {
delete this.cache[name]
}
}
}
提示:
tasks=this.cache[name].slice() 利用.slice()返回一个拥有cache[name]所有元素的子数组赋值给tasks
slice() 方法可从已有的数组中返回选定的元素。
语法:
arrayObject.slice(start,end)
- start 必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
- end 可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
- 返回值 返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。 注意:
该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法 Array.splice()。
总结
解释了这么多,下面的测试就自己去试着去推一下呗,其实很简单的,毕竟学习是自己的事情,别害臊,小伙伴们,加油加油,我相信你们,加油加油。