手写代码:实现一个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实例
相关文章
|
Java 数据库连接 Spring
JavaWeb优雅实现接口参数校验
JavaWeb优雅实现接口参数校验
268 0
|
算法 安全 Linux
二进制代码保护和混淆
常见二进制代码保护和混淆的概述
629 0
二进制代码保护和混淆
|
安全 计算机视觉
高通TrustZone接口QSEECOM Use-After-Free漏洞分析
#高通QSEECOM接口漏洞(CVE-2019-14040)分析 #阿里安全(侯客) ##背景:  上周五看到一篇国外的安全公司zimperium的研究人员写的一篇他们分析发现的高通的QSEECOM接口漏洞文章,[https://blog.zimperium.com/multiple-kernel-vulnerabilities-affecting-all-qualcomm-d
2106 0
高通TrustZone接口QSEECOM Use-After-Free漏洞分析
|
7月前
|
人工智能 供应链 API
反向海淘实战:Pandabuy、Hoobuy、CNFans 代购集运系统搭建真实体验
2025年,反向海淘成为新趋势。CSDN博主耗时2个月,模拟留学生、海外华人等场景,深度体验Pandabuy、Hoobuy、CNFans三大代购平台。Pandabuy极简易用,Hoobuy稳健实用,CNFans技术强大。通过真实案例分析,探讨各平台优劣及未来AI发展趋势,帮助用户避开常见陷阱,选择最适合的购物方案。
1117 1
|
11月前
|
UED 开发者 容器
鸿蒙next版开发:ArkTS组件通用属性(背景设置)
在HarmonyOS 5.0中,ArkTS提供了多种背景设置属性,如backgroundColor、backgroundImage、backgroundSize、backgroundPosition和backgroundBlurStyle,允许开发者自定义组件的背景样式,提升应用的视觉效果和用户体验。本文详细解读了这些属性,并提供了示例代码进行说明。
1098 5
|
开发工具 git
Git 中 merge 和 rebase 的区别
$ git pull --rebase和$ git pull区别 是git fetch + git merge FETCH_HEAD的缩写,所以默认情况下,git pull就是先fetch,然后执行merge操作,如果加-rebase参数,就是使用git rebase代替git merge 。
29612 0
|
Web App开发 数据安全/隐私保护
防止打开控制台
防止打开控制台
696 0
|
开发工具 iOS开发 git
Mac Homebrew 安装与卸载
Mac Homebrew 安装与卸载
13629 0
|
JavaScript 前端开发
浏览器中的事件循环和Node.js中事件循环的区别(经典面试题)
浏览器中的事件循环和Node.js中事件循环的区别(经典面试题)
1217 0
|
前端开发 JavaScript
路径中“./”、“../”、“/”代表的含义
路径中“./”、“../”、“/”代表的含义
672 0