最近參加了一次螞蟻金服的面試,其中有兩道筆試題,分別是手寫事件總線和手寫模板引擎
手寫模板引擎比較復雜,除了需要識別 {{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); } }
