手写代码:实现一个EventBus

简介: EventBus,事件总线。总线一词来自于《计算机组成原理》中的”系统总线“,是指用于连接多个部件的信息传输线,各部件共享的传输介质。我们通常把事件总线也成为自定义事件,一般包含`on`、`once`、`emit`、`off`等方法。在Vue2中想要实现EventBus比较简单,直接暴露出一个`new Vue()`实例即可,以此为思路,我们应该如何自定义实现EventBus呢?

EventBus,事件总线。总线一词来自于《计算机组成原理》中的”系统总线“,是指用于连接多个部件的信息传输线,各部件共享的传输介质。我们通常把事件总线也成为自定义事件,一般包含ononceemitoff等方法。在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);

执行结果

image-20220629161212532.png

过程分析

  • 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;
  }
  ...
}

我们再来执行测试案例:

image-20220629162320631.png

结果就是我们所预期的。

总结

本文提供了一种实现EventBus的方法,其实也并不复杂,需要注意几点:

  • 使用数组保证函数有序,使用对象便于获取
  • 由于存在once,需要在函数执行后将其从函数数组中过滤掉
  • 需要实现单例模式,保证全局只有一个EventBus实例
相关文章
|
前端开发 JavaScript
React17源码解读—— 事件系统
读完本篇文章你将明白为什么是React的合成事件SyntheticEvent, 以及React如何模拟浏览器的捕获和冒泡。   在学习React的合成事件之前,我们先复习下浏览器的事件系统,以及代理委托。这对我理解React事件系统源码非常重要。   W3C 标准约定了一个事件的传播过程要经过以下 3 个阶段:
React17源码解读—— 事件系统
|
JavaScript 中间件 API
redux是怎样做异步请求的?
redux是怎样做异步请求的?
|
设计模式 存储 JavaScript
前端面试100道手写题(3)—— EventBus
EventBus作为发布订阅设计模式的经典应用场景,很值得我们去学习研究它的实现原理。
165 0
|
前端开发 JavaScript API
继续解惑,异步处理 —— RxJS Observable
Observable 可观察对象是开辟一个连续的通信通道给观察者 Observer,彼此之前形成一种关系,而这种关系需要由 Subscription 来确立,而在整个通道中允许对数据进行转换我们称为操作符 Operator。
探秘 RxJS Observable 为什么要长成这个样子?!
我们都知道 RxJS Observable 最基础的使用方法:是建立 Observable,即调用 .create API
|
Android开发 索引
EventBus封装到项目架构|青训营笔记
封装该库到自己的项目的目的有两个 便捷绑定和解绑 EventBus 便捷通过 EventBus 发送消息和处理消息 代码美观
EventBus封装到项目架构|青训营笔记
|
设计模式 缓存 Android开发
换个姿势,更好地参透EventBus(上)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
251 1
|
安全 Java API
换个姿势,更好地参透EventBus(下)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
129 0
|
设计模式 存储 Java
换个姿势,更好地参透EventBus(中)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
111 0
|
监控 Java
说下你可能没用过的EventBus
一般情况下,我们会做成异步的方式,使用MQ自己发送自己消费,或者说一个线程池搞定,这样的话不影响主业务逻辑,可以提高性能,并且代码做到了解耦。