面試官:既然React/Vue可以用Event Bus進行組件通信,你可以實現下嗎?

前言

本文標題的題目是由其他問題延伸而來,面試中面試官的常用套路,揪住一個問題一直深挖,在產生這個問題之前一定是這個問題.

React/Vue不同組件之間是怎麼通信的?

Vue

  1. 父子組件用Props通信

  2. 非父子組件用Event Bus通信

  3. 如果項目夠複雜,可能需要Vuex等全局狀態管理庫通信

  4. $dispatch(已經廢除)和$broadcast(已經廢除)

React

  1. 父子組件,父->子直接用Props,子->父用callback回調

  2. 非父子組件,用發佈訂閱模式的Event模塊

  3. 項目複雜的話用Redux、Mobx等全局狀態管理管庫

  4. 用新的Context Api

我們大體上都會有以上回答,接下來很可能會問到如何實現Event(Bus),因爲這個東西太重要了,幾乎所有的模塊通信都是基於類似的模式,包括安卓開發中的Event Bus,Node.js中的Event模塊(Node中幾乎所有的模塊都依賴於Event,包括不限於http、stream、buffer、fs等).

我們仿照Node中Event API實現一個簡單的Event庫,他是發佈訂閱模式的典型應用.

提前聲明: 我們沒有對傳入的參數進行及時判斷而規避錯誤,僅僅對核心方法進行了實現.


1.基本構造

1.1初始化class

我們利用ES6的class關鍵字對Event進行初始化,包括Event的事件清單和監聽者上限.

我們選擇了Map作爲儲存事件的結構,因爲作爲鍵值對的儲存方式Map比一般對象更加適合,我們操作起來也更加簡潔,可以先看一下Map的基本用法與特點.

class EventEmeitter {  
     constructor() { 
    this._events = this._events || new Map(); // 儲存事件/回調鍵值對   
    this._maxListeners = this._maxListeners || 10; // 設立監聽上限   
    }
}

1.2 監聽與觸發

觸發監聽函數我們可以用applycall兩種方法,在少數參數時call的性能更好,多個參數時apply性能更好,當年Node的Event模塊就在三個參數以下用call否則用apply.

當然當Node全面擁抱ES6+之後,相應的call/apply操作用Reflect新關鍵字重寫了,但是我們不想寫的那麼複雜,就做了一個簡化版.

 // 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args){  
         let handler;   // 從儲存事件鍵值對的this._events中獲取對應事件回調函數 
        handler = this._events.get(type); 
       if (args.length > 0) {   
           handler.apply(this, args);  
       }else{ 
          handler.call(this); 
       }  
       return true;
};
 // 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
     // 將type事件以及對應的fn函數放入this._events中儲存   
   if (!this._events.get(type)) {  
     this._events.set(type, fn); 
   }
};

我們實現了觸發事件的emit方法和監聽事件的addListener方法,至此我們就可以進行簡單的實踐了.

// 實例化 const emitter = new EventEmeitter();
// 監聽一個名爲arson的事件對應一個回調函數
emitter.addListener('arson', man => {  
 console.log(`expel ${man}`); 
});
 // 我們觸發arson事件,發現回調成功執行 
emitter.emit('arson', 'low-end'); // expel low-end

似乎不錯,我們實現了基本的觸發/監聽,但是如果有多個監聽者呢?

// 重複監聽同一個事件名
 emitter.addListener('arson', man => { 
   console.log(`expel ${man}`);
}); 
emitter.addListener('arson', man => { 
  console.log(`save ${man}`); 
}); 
emitter.emit('arson', 'low-end'); // expel low-end

是的,只會觸發第一個,因此我們需要進行改造.


2.升級改造

2.1 監聽/觸發器升級

我們的addListener實現方法還不夠健全,在綁定第一個監聽者之後,我們就無法對後續監聽者進行綁定了,因此我們需要將後續監聽者與第一個監聽者函數放到一個數組裏.

// 觸發名爲type的事件 
EventEmeitter.prototype.emit = function(type, ...args) { 
  let handler; 
  handler = this._events.get(type);  
  if (Array.isArray(handler)) { 
  // 如果是一個數組說明有多個監聽者,需要依次此觸發裏面的函數  
      for (let i = 0; i < handler.length; i++) {  
          if (args.length > 0) {   
              handler[i].apply(this, args);  
          } else {      
                  handler[i].call(this);   
          }  
        }  
 } else {
  // 單個函數的情況我們直接觸發即可 
      if (args.length > 0) {   
               handler.apply(this, args);  
          } else {   
            handler.call(this); 
      }  
  }  
   return true;
 };
  // 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) { 
     const handler = this._events.get(type); 
     // 獲取對應事件名稱的函數清單  
      if (!handler) {   
       this._events.set(type, fn);
      } else if (handler && typeof handler === 'function') { 
         // 如果handler是函數說明只有一個監聽者   
          this._events.set(type, [handler, fn]); 
          // 多個監聽者我們需要用數組儲存  
      } else {  
         handler.push(fn);
          // 已經有多個監聽者,那麼直接往數組裏push函數即可 
      }
};

是的,從此以後可以愉快的觸發多個監聽者的函數了.

// 監聽同一個事件名 
emitter.addListener('arson', man => { 
        console.log(`expel ${man}`);
 }); 
emitter.addListener('arson', man => { 
    console.log(`save ${man}`);
 }); 
emitter.addListener('arson', man => { 
   console.log(`kill ${man}`);
 }); 
 // 觸發事件 
 emitter.emit('arson', 'low-end'); //expel low-end //save low-end //kill low-end

2.2 移除監聽

我們會用removeListener函數移除監聽函數,但是匿名函數是無法移除的.

EventEmeitter.prototype.removeListener = function(type, fn) {  
 const handler = this._events.get(type); // 獲取對應事件名稱的函數清單 
   // 如果是函數,說明只被監聽了一次  
 if (handler && typeof handler === 'function') {   
   this._events.delete(type, fn);  
  }else{   
    let postion;     // 如果handler是數組,說明被監聽多次要找到對應的函數    
     for (let i = 0; i < handler.length; i++) {  
        if (handler[i] === fn) {        
           postion = i;      
        }else{   
            postion = -1;   
        }   
     }     // 如果找到匹配的函數,從數組中清除  
     if(postion !== -1){     
       // 找到數組對應的位置,直接清除此回調  
       handler.splice(postion, 1);      
        // 如果清除後只有一個函數,那麼取消數組,以函數形式保存   
       if (handler.length === 1){        
           this._events.set(type, handler[0]);     
        }     
    }else{  
         return this;  
       }   
   } 
   };

3.發現問題

我們已經基本完成了Event最重要的幾個方法,也完成了升級改造,可以說一個Event的骨架是被我們開發出來了,但是它仍然有不足和需要補充的地方.

  1. 魯棒性不足: 我們沒有對參數進行充分的判斷,沒有完善的報錯機制.

  2. 模擬不夠充分: 除了removeAllListeners這些方法沒有實現以外,例如監聽時間後會觸發newListener事件,我們也沒有實現,另外最開始的監聽者上限我們也沒有利用到.

當然,這在面試中現場寫一個Event已經是很夠意思了,主要是體現出來對發佈-訂閱模式的理解,以及針對多個監聽狀況下的處理,不可能現場擼幾百行寫一個完整Event.

索性Event庫幫我們實現了完整的特性,整個代碼量有300多行,很適合閱讀,你可以花十分鐘的時間通讀一下,見識一下完整的Event實現.


作者:尋找海藍96
鏈接:https://juejin.im/post/5ac2fb886fb9a028b86e328c


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章