前端面试(蚂蚁金服笔试) - 手写事件总线 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);
}
}