最近参加了一次蚂蚁金服的面试,其中有两道笔试题,分别是手写事件总栈和手写模板引擎
手写模板引擎比较复杂,除了需要识别 {{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); } }