前端面試(螞蟻金服筆試) - 手寫事件總線 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); } }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM