事件总线(发布订阅模式)

3,460 阅读5分钟

前言

事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉。事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。

我们来看看事件总线的处理流程:

5.png

看到这里我知道肯定有些小伙伴还是有点蒙吧(其实我是蒙的),没关系,我们一步一步来,不急

事件的本质

我们先来探讨一下事件的概念。

开发过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实现委托。 总的来说,发布订阅模式中有两个关键字,通知和更新。 被观察者状态改变通知观察者做出相应更新。 解决的是当对象改变时需要通知其他对象做出相应改变的问题。

口述态复杂了,我们还是来看图吧!

6.png

好啦好啦,前菜上完了,该上正餐了。

事件总线(发布订阅模式)

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()。

总结

解释了这么多,下面的测试就自己去试着去推一下呗,其实很简单的,毕竟学习是自己的事情,别害臊,小伙伴们,加油加油,我相信你们,加油加油。