前端面试(蚂蚁金服笔试) - 手写事件总栈 EventBus

最近参加了一次蚂蚁金服的面试,其中有两道笔试题,分别是手写事件总栈和手写模板引擎

手写模板引擎比较复杂,除了需要识别 {{data.name}} 这种基本情况之外, 还要兼顾 {{data.info[1]}}{{data.others["about"]}}

于是先记录下手写事件总栈,后面再完善手写模板引擎的代码

 

一、什么是事件总栈

在 Vue 2.x 中,有两种能在任意组件中传参的方式:状态管理 Vuex 和事件总栈 EventBus

但 EventBus 并非 Vue 首创,它作为一种事件的发布订阅模式,一直活跃在各种代码框架中

EventBus 化了各个组件之间进行通信的复杂度,其工作原理在于对事件的监听与手动触发:

// 实例化事件总栈
const events = new EventBus();

// 监听自定义事件
events.on('my-event', (value) => {
  console.log(value);
});

// 触发事件
events.emit('my-event', 'helloworld');

而这种先注册事件监听函数,然后通过触发事件来传参的行为其实是一种发布订阅模式 

 

二、发布订阅模式

发布订阅模式是一种广泛应用于异步编程的模式,是回调函数的事件化,常常用来解耦业务逻辑

作为一个事件总栈,它应当具备一个任务队列,以及三个方法:订阅方法、发布方法、取消订阅

class EventBus {
  constructor() {
    this.tasks = {}; // 按事件名称创建任务队列
  }
  // 注册事件(订阅)
  on() {}
  // 触发事件(发布)
  emit() {}
  // 移除指定回调(取消订阅)
  off() {}
}

 

首先来实现订阅方法 on()

它的作用是将事件的处理函数加入任务队列,所以需要接收两个参数:事件名称、事件的处理函数

/**
 * 注册事件(订阅)
 * @param {String} type  事件名称
 * @param {Function} fn  回调函数
 */
on(type, fn) {
  // 如果还没有注册过该事件,则创建对应事件的队列
  if (!this.tasks[type]) {
    this.tasks[type] = [];
  }
  // 将回调函数加入队列
  this.tasks[type].push(fn);
}

 

然后是触发事件的方法 emit()

其功能是每触发一次事件,会执行对应事件的所有回调函数。所以它的参数必须有一个是事件名称,另外还可以传入一个参数作为回调函数的参数

/**
 * 触发事件(发布)
 * @param {String} type  事件名称
 * @param {...any} args  传入的参数,不限个数
 */
emit(type, ...args) {
  // 如果该事件没有被注册,则返回
  if (!this.tasks[type]) {
    return;
  }
  // 遍历执行对应的回调数组,并传入参数
  this.tasks[type].forEach(fn => fn(...args));
}

 

最后是注销方法 off(),它将需要注销的事件处理函数从对应事件的任务队列中清除

/**
 * 移除指定回调(取消订阅)
 * @param {String} type  事件名称
 * @param {Function} fn  回调函数
 */
off(type, fn) {
  const tasks = this.tasks[type];
// 校验事件队列是否存在 if (!Array.isArray(tasks)) { return; } // 利用 filter 删除队列中的指定函数 this.tasks[type] = tasks.filter(cb => fn !== cb); }

 到这里一个简单的事件总栈就已经完成,可以通过第一部分的测试代码进行测试

 

三、完整代码

通常事件总栈内除了上面提到的三种方法外,还会包含一个 once() 方法,用来注册一个只能执行一次的事件,会在下面的代码中体现

class EventBus {
  constructor() {
    this.tasks = {}; // 按事件名称创建任务队列
  }

  /**
   * 注册事件(订阅)
   * @param {String} type  事件名称
   * @param {Function} fn  回调函数
   */
  on(type, fn) {
    // 如果还没有注册过该事件,则创建对应事件的队列
    if (!this.tasks[type]) {
      this.tasks[type] = [];
    }
    // 将回调函数加入队列
    this.tasks[type].push(fn);
  }
  
  /**
   * 注册一个只能执行一次的事件
   * @params type[String] 事件类型
   * @params fn[Function] 回调函数
   */
  once(type, fn) {
    if (!this.tasks[type]) {
      this.tasks[type] = [];
    }

    const that = this;
    // 注意该函数必须是具名函数,因为需要删除,但该名称只在函数内部有效
    function _once(...args) {
      fn(...args);
      that.off(type, _once); // 执行一次后注销
    }

    this.tasks[type].push(_once);
  }

  /**
   * 触发事件(发布)
   * @param {String} type  事件名称
   * @param {...any} args  传入的参数,不限个数
   */
  emit(type, ...args) {
    // 如果该事件没有被注册,则返回
    if (!this.tasks[type]) {
      return;
    }
    // 遍历执行对应的回调数组,并传入参数
    this.tasks[type].forEach((fn) => fn(...args));
  }

  /**
   * 移除指定回调(取消订阅)
   * @param {String} type  事件名称
   * @param {Function} fn  回调函数
   */
  off(type, fn) {
    const tasks = this.tasks[type];
    // 校验事件队列是否存在
    if (!Array.isArray(tasks)) {
      return;
    }

    // 利用 filter 删除队列中的指定函数
    this.tasks[type] = tasks.filter((cb) => fn !== cb);
  }
}

 

上一篇:一篇文章教你搞定计算机网络面试,全套教学资料


下一篇:一篇文章教你搞定计算机网络面试,已开源