看完vue2的EventBus,这不手写一个?

143 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情

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实例