成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如果面試官讓你講講發布訂閱設計模式?

開發 前端
發布訂閱設計模式在程序中經常涉及,例如 Vue 中的 $on 和 $off、document.addEventListener()、document.removeEventListener()等,發布訂閱模式可以降低程序的耦合度,統一管理維護消息、處理事件也使得程序更容易維護和擴展。

[[414875]]

本文轉載自微信公眾號「DYBOY」,作者DYBOY。轉載本文請聯系DYBOY公眾號。

發布訂閱設計模式在程序中經常涉及,例如 Vue 中的 $on 和 $off、document.addEventListener()、document.removeEventListener()等,發布訂閱模式可以降低程序的耦合度,統一管理維護消息、處理事件也使得程序更容易維護和擴展。

有小伙伴問,該如何學習設計模式,設計模式本身是一些問題場景的抽象解決方案,死記硬背肯定不行,無異于搭建空中樓閣,所以得結合實際,從解決問題角度去思考、舉一反三,如此便能更輕松掌握知識點。

最近在程序中使用到了 eventEmitter3 這個事件發布訂閱庫,該庫可用于組件之間的通信管理,通過簡單的 Readme 文檔可學會如何使用,但同時了解這個庫的設計也有助于大家了解認識發布訂閱設計模式,不妨一起來看看。

一、定義

在軟件架構中,發布訂閱是一種消息范式,消息的發送者(稱為發布者)不會將消息直接發送給特定的接收者(稱為訂閱者),而是將發布的消息分為不同的類別,無需了解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類別的興趣,只接收感興趣的消息,無需了解哪些發布者(如果有的話)存在。

類比一個很好理解的例子,例如微信公眾號,你關注(理解為訂閱)了“DYBOY”公眾號,當該公眾號發布了新文章,微信就會通知你,而不會通知其他為訂閱公眾號的人,另外你還可以訂閱多個公眾號。

放到程序的組件中,多個組件的通信除了父子組件傳值外,還有例如 redux、vuex 狀態管理,另外就是本文所說的發布訂閱模式,可以通過一個事件中心來實現。

發布訂閱模式

二、手搓一個發布訂閱事件中心

“紙上得來終覺淺,絕知此事要躬行”,所以根據定義,我們嘗試實現一個JavaScript版本的發布訂閱事件中心,看看會遇到哪些問題?

2.1 基本結構版

首先實現的 DiyEventEmitter 如下:

  1. /** 
  2.  * 事件發布訂閱中心 
  3.  */ 
  4. class DiyEventEmitter { 
  5.   static instance: DiyEventEmitter; 
  6.   private _eventsMap: Map<string, Array<() => void>>; 
  7.  
  8.   static getInstance() { 
  9.     if (!DiyEventEmitter.instance) { 
  10.       DiyEventEmitter.instance = new DiyEventEmitter(); 
  11.     } 
  12.     return DiyEventEmitter.instance; 
  13.   } 
  14.  
  15.   constructor() { 
  16.     this._eventsMap = new Map(); // 事件名與回調函數的映射Map 
  17.   } 
  18.  
  19.   /** 
  20.    * 事件訂閱 
  21.    * 
  22.    * @param eventName 事件名 
  23.    * @param eventFnCallback 事件發生時的回調函數 
  24.    */ 
  25.   public on(eventName: string, eventFnCallback: () => void) { 
  26.     const newArr = this._eventsMap.get(eventName) || []; 
  27.     newArr.push(eventFnCallback); 
  28.     this._eventsMap.set(eventName, newArr); 
  29.   } 
  30.  
  31.   /** 
  32.    * 取消訂閱 
  33.    * 
  34.    * @param eventName 事件名 
  35.    * @param eventFnCallback 事件發生時的回調函數 
  36.    */ 
  37.   public off(eventName: string, eventFnCallback?: () => void) { 
  38.     if (!eventFnCallback) { 
  39.       this._eventsMap.delete(eventName); 
  40.       return
  41.     } 
  42.  
  43.     const newArr = this._eventsMap.get(eventName) || []; 
  44.     for (let i = newArr.length - 1; i >= 0; i--) { 
  45.       if (newArr[i] === eventFnCallback) { 
  46.         newArr.splice(i, 1); 
  47.       } 
  48.     } 
  49.     this._eventsMap.set(eventName, newArr); 
  50.   } 
  51.  
  52.   /** 
  53.    * 主動通知并執行注冊的回調函數 
  54.    * 
  55.    * @param eventName 事件名 
  56.    */ 
  57.   public emit(eventName: string) { 
  58.     const fns = this._eventsMap.get(eventName) || []; 
  59.     fns.forEach(fn => fn()); 
  60.   } 
  61.  
  62. export default DiyEventEmitter.getInstance(); 

導出的 DiyEventEmitter 是一個“單例”,保證在全局中只有唯一“事件中心”實例,使用時候直接可使用公共方法

  1. import e from "./DiyEventEmitter"
  2.  
  3. const subscribeFn = () => { 
  4.   console.log("DYBOY訂閱收到了消息"); 
  5. }; 
  6. const subscribeFn2 = () => { 
  7.   console.log("DYBOY第二個訂閱收到了消息"); 
  8. }; 
  9.  
  10. // 訂閱 
  11. e.on("dyboy", subscribeFn); 
  12. e.on("dyboy", subscribeFn2); 
  13.  
  14. // 發布消息 
  15. e.emit("dyboy"); 
  16.  
  17. // 取消第一個訂閱消息的綁定 
  18. e.off("dyboy", subscribeFn); 
  19.  
  20. // 第二次發布消息 
  21. e.emit("dyboy"); 

輸出 console 結果:

  1. DYBOY訂閱收到了消息 
  2. 第二個訂閱的消息 
  3. 第二個訂閱的消息 

那么第一版的支持訂閱、發布、取消的“發布訂閱事件中心”就OK了。

2.2 支持只訂閱一次once方法

在一些場景下,某些事件訂閱可能只需要執行一次,后續的通知將不再響應。

實現的思路:新增 once 訂閱方法,當響應了對應“發布者消息”,則主動取消訂閱當前執行的回調函數。

為此新增類型,如此便于回調函數的描述信息擴展:

  1. type SingleEvent = { 
  2.   fn: () => void; 
  3.   once: boolean; 
  4. }; 

_eventsMap的類型更改為:

  1. private _eventsMap: Map<string, Array<SingleEvent>>; 

同時抽出公共方法 addListener,供 on 和 once 方法共用:

  1. private addListener( eventName: string, eventFnCallback: () => void, once = false) { 
  2.   const newArr = this._eventsMap.get(eventName) || []; 
  3.   newArr.push({ 
  4.     fn: eventFnCallback, 
  5.     once, 
  6.   }); 
  7.   this._eventsMap.set(eventName, newArr); 
  8.  
  9. /** 
  10.  * 事件訂閱 
  11.  * 
  12.  * @param eventName 事件名 
  13.  * @param eventFnCallback 事件發生時的回調函數 
  14.  */ 
  15. public on(eventName: string, eventFnCallback: () => void) { 
  16.   this.addListener(eventName, eventFnCallback); 
  17.  
  18. /** 
  19.  * 事件訂閱一次 
  20.  * 
  21.  * @param eventName 事件名 
  22.  * @param eventFnCallback 事件發生時的回調函數 
  23.  */ 
  24. public once(eventName: string, eventFnCallback: () => void) { 
  25.   this.addListener(eventName, eventFnCallback, true); 

與此同時,我們需要考慮在觸發事件時候,執行一次就需要取消訂閱

  1. /** 
  2.  * 觸發:主動通知并執行注冊的回調函數 
  3.  * 
  4.  * @param eventName 事件名 
  5.  */ 
  6. public emit(eventName: string) { 
  7.   const fns = this._eventsMap.get(eventName) || []; 
  8.   fns.forEach((evt, index) => { 
  9.     evt.fn(); 
  10.     if (evt.once) fns.splice(index, 1); 
  11.   }); 
  12.   this._eventsMap.set(eventName, fns); 

另外取消訂閱中函數中比較需要替換對象屬性比較:newArr[i].fn === eventFnCallback

這樣我們的事件中心支持 once 方法改造就完成了。

2.3 緩存發布消息

在框架開發下,通常會使用異步按需加載組件,如果發布者組件先發布了消息,但是異步組件還未加載完成(完成訂閱注冊),那么發布者的這條發布消息就不會被響應。因此,我們需要把消息做一個緩存隊列,直到有訂閱者訂閱了,并只響應一次緩存的發布消息,該消息就會從緩存出隊。

首先梳理下緩存消息的邏輯流程:

UML時序圖

發布者發布消息,事件中心檢測是否存在訂閱者,如果沒有訂閱者訂閱此條消息,則把該消息緩存到離線消息隊列中,當有訂閱者訂閱時,檢測是否訂閱了緩存中的事件消息,如果是,則該事件的緩存消息依次出隊(FCFS調度執行),觸發訂閱者回調函數執行一次。

新增離線消息緩存隊列:

  1. private _offlineMessageQueue: Map<string, number>; 

在emit發布消息中判斷對應事件是否有訂閱者,沒有訂閱者則向離線事件消息中更新

  1. /** 
  2.  * 觸發:主動通知并執行注冊的回調函數 
  3.  * 
  4.  * @param eventName 事件名 
  5.  */ 
  6. public emit(eventName: string) { 
  7.   const fns = this._eventsMap.get(eventName) || []; 
  8. +  if (fns.length === 0) { 
  9. +    const counter = this._offlineMessageQueue.get(eventName) || 0; 
  10. +    this._offlineMessageQueue.set(eventName, counter + 1); 
  11. +    return
  12. +  } 
  13.   fns.forEach((evt, index) => { 
  14.     evt.fn(); 
  15.     if (evt.once) fns.splice(index, 1); 
  16.   }); 
  17.   this._eventsMap.set(eventName, fns); 

然后在 addListener 方法中根據離線事件消息統計的次數,重新emit發布事件消息,觸發消息回調函數執行,之后刪掉離線消息中的對應事件。

  1. private addListener( 
  2.   eventName: string, 
  3.   eventFnCallback: () => void, 
  4.   once = false 
  5. ) { 
  6.   const newArr = this._eventsMap.get(eventName) || []; 
  7.   newArr.push({ 
  8.     fn: eventFnCallback, 
  9.     once, 
  10.   }); 
  11.   this._eventsMap.set(eventName, newArr); 
  12.  
  13. +  const cacheMessageCounter = this._offlineMessageQueue.get(eventName); 
  14. +  if (cacheMessageCounter) { 
  15. +    for (let i = 0; i < cacheMessageCounter; i++) { 
  16. +      this.emit(eventName); 
  17. +    } 
  18. +    this._offlineMessageQueue.delete(eventName); 
  19. +  } 

這樣,一個支持離線消息的事件中心就寫好了!

2.4 回調函數傳參&執行環境

在上面的回調函數中,我們可以發現是一個沒有返回值,沒有入參的函數,這其實有些雞肋,在函數運行的時候會指向執行的上下文,可能某些回調函數中含有this指向就無法綁定到事件中心上,因此針對回調函數需要綁定執行上下文環境。

2.4.1 支持回調函數傳參

首先將TypeScript中的函數類型fn: () => void 改為 fn: Function,這樣能夠通過函數任意參數長度的TS校驗。

其實在事件中心里回調函數是沒有參數的,如有參數也是提前通過參數綁定(bind)方式傳入。

另外如果真要支持回調函數傳參,那么就需要在 emit() 的時候傳入參數,然后再將參數傳遞給回調函數,這里我們暫時先不實現了。

2.4.2 執行環境綁定

在需要實現執行環境綁定這個功能前,先思考一個問題:“是應該開發者自行綁定還是應該事件中心來做?”

換句話說,開發者在 on('eventName', 回調函數) 的時候,是否應該主動綁定 this 指向?在當前設計下,初步認為無參數的回調函數自行綁定 this 比較合適。

因此,在事件中心這暫時不需要去做綁定參數的行為,如果回調函數內有需要傳參、綁定執行上下文的,需要在綁定回調函數的時候自行 bind。這樣,我們的事件中心也算是保證了功能的純凈性。

到這里我們自己手搓簡單的發布訂閱事件中心就完成了!

三、學習EventEmitter3的設計實現

雖然我們按照自己的理解實現了一版,但是沒有對比我們也不知道好壞,因此一起看看 EventEmitter3 這個優秀“極致性能優化”的庫是怎么去處理事件訂閱與發布,同時可以學習下其中的性能優化思路。

首先,EventEmitter3(后續簡稱:EE3)的實現思路,用Events對象作為“回調事件對象”的存儲器,類比我們上述實現的“發布訂閱模式”作為事件的執行邏輯,另外addListener() 函數增加了傳入執行上下文環境參數,emit() 函數支持最多傳入5個參數,同時EventEmitter3中還加入了監聽器計數、事件名前綴。

3.1 Events存儲器

避免轉譯,以及為了提升兼容性和性能,EventEmitter3用ES5來編寫。

在JavaScript中萬物是對象,函數也是對象,因此存儲器的實現:

  1. function Events() {} 

3.2 事件偵聽器實例

同理,我們上述使用singleEvent對象來存儲每一個事件偵聽器實例,EE3 中用一個EE對象存儲每個事件偵聽器的實例以及必要屬性

  1. /** 
  2.  * 每個事件偵聽器實例的表示形式 
  3.  * 
  4.  * @param {Function} fn 偵聽器函數 
  5.  * @param {*} context 調用偵聽器的執行上下文 
  6.  * @param {Boolean} [once=false] 指定偵聽器是否僅支持調用一次 
  7.  * @constructor 
  8.  * @private 
  9.  */ 
  10. function EE(fn, context, once) { 
  11.   this.fn = fn; 
  12.   this.context = context; 
  13.   this.once = once || false

3.3 添加偵聽器方法

  1. /** 
  2.  * 為給定事件添加偵聽器 
  3.  * 
  4.  * @param {EventEmitter} emitter EventEmitter實例的引用. 
  5.  * @param {(String|Symbol)} event 事件名. 
  6.  * @param {Function} fn 偵聽器函數. 
  7.  * @param {*} context 調用偵聽器的上下文. 
  8.  * @param {Boolean} once 指定偵聽器是否僅支持調用一次. 
  9.  * @returns {EventEmitter} 
  10.  * @private 
  11.  */ 
  12. function addListener(emitter, event, fn, context, once) { 
  13.   if (typeof fn !== 'function') { 
  14.     throw new TypeError('The listener must be a function'); 
  15.   } 
  16.  
  17.   var listener = new EE(fn, context || emitter, once) 
  18.     , evt = prefix ? prefix + event : event; 
  19.  
  20.   // TODO: 這里為什么先是使用對象,多個的時候使用對象數組存儲,有什么好處? 
  21.   if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 
  22.   else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 
  23.   else emitter._events[evt] = [emitter._events[evt], listener]; 
  24.  
  25.   return emitter; 

該“添加偵聽器”的方法有幾個關鍵功能點:

如果有前綴,給事件名增加前綴,避免事件沖突

每次新增事件名則 _eventsCount+1,用于快速讀寫所有事件的數量

如果事件只有單個偵聽器,則 _events[evt] 指向這個 EE 對象,訪問效率更高

3.4 清除事件

  1. /** 
  2.  * 通過事件名清除事件 
  3.  * 
  4.  * @param {EventEmitter} emitter EventEmitter實例的引用 
  5.  * @param {(String|Symbol)} evt 事件名 
  6.  * @private 
  7.  */ 
  8. function clearEvent(emitter, evt) { 
  9.   if (--emitter._eventsCount === 0) emitter._events = new Events(); 
  10.   else delete emitter._events[evt]; 

清除事件,只需要使用 delete 關鍵字,刪除對象上的屬性

另外這里一個很巧妙的地方在于,依賴事件計數器,如果計數器為0,則重新創建一個 Events 存儲器指向 emitter 的 _events 屬性。

這樣做的優點是,假如需要清空所有事件,只需要將 emitter._eventsCount 的值賦值為1,然后調用 clearEvent() 方法就可以了,而不必遍歷清除事件

3.5 EventEmitter

  1. function EventEmitter() { 
  2.   this._events = new Events(); 
  3.   this._eventsCount = 0; 

EventEmitter 對象參考 NodeJS 中的事件觸發器,定義了最小的接口模型,包含 _events 和 _eventsCount屬性,另外的方法都通過原型來增加。

EventEmitter 對象等同于上述我們的事件中心的定義,其功能梳理如下:

EventEmitter

其中有必要講的就是 emit() 方法,而訂閱者注冊事件的on() 和 once() 方法,都是使用的 addListener() 工具函數。

emit() 方法實現如下:

  1. /** 
  2.  * 調用執行指定事件名的每一個偵聽器 
  3.  * 
  4.  * @param {(String|Symbol)} event 事件名. 
  5.  * @returns {Boolean} `true` 如果當前事件名沒綁定偵聽器,則返回false
  6.  * @public 
  7.  */ 
  8. EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 
  9.   var evt = prefix ? prefix + event : event; 
  10.  
  11.   if (!this._events[evt]) return false
  12.  
  13.   var listeners = this._events[evt] 
  14.     , len = arguments.length 
  15.     , args 
  16.     , i; 
  17.  
  18.   // 如果只有一個偵聽器綁定了該事件名 
  19.   if (listeners.fn) { 
  20.     // 如果是執行一次的,則移除偵聽器 
  21.     if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 
  22.      
  23.     // Refrence:https://juejin.cn/post/6844903496450310157 
  24.     // 這里的處理是從性能上考慮,傳入5個入參數的調用call方法處理 
  25.     // 超過5個參數的使用apply處理 
  26.     // 大部分場景超過5個參數的都是少數 
  27.     switch (len) { 
  28.       case 1: return listeners.fn.call(listeners.context), true
  29.       case 2: return listeners.fn.call(listeners.context, a1), true
  30.       case 3: return listeners.fn.call(listeners.context, a1, a2), true
  31.       case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true
  32.       case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true
  33.       case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true
  34.     } 
  35.  
  36.     for (i = 1, args = new Array(len -1); i < len; i++) { 
  37.       args[i - 1] = arguments[i]; 
  38.     } 
  39.  
  40.     listeners.fn.apply(listeners.context, args); 
  41.   } else { 
  42.     // 當有多個偵聽器綁定了同一個事件名 
  43.     var length = listeners.length 
  44.       , j; 
  45.      
  46.     // 循環執行每一個綁定的事件偵聽器 
  47.     for (i = 0; i < length; i++) { 
  48.       if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 
  49.  
  50.       switch (len) { 
  51.         case 1: listeners[i].fn.call(listeners[i].context); break; 
  52.         case 2: listeners[i].fn.call(listeners[i].context, a1); break; 
  53.         case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 
  54.         case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 
  55.         default
  56.           if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 
  57.             args[j - 1] = arguments[j]; 
  58.           } 
  59.           listeners[i].fn.apply(listeners[i].context, args); 
  60.       } 
  61.     } 
  62.   } 
  63.  
  64.   return true
  65. }; 

在 emit() 方法中顯示的傳入了五個入參:a1 ~ a5,同時優先使用 call() 方法綁定 this 指向并執行偵聽器的回調函數。

這樣處理的原因是,call 方法比 apply 方法效率更高,相關比較驗證討論可參考《call和apply的性能對比》

到這基本上 EventEmitter3 的實現就啃完了!

四、總結

EventEmitter3 是一個號稱優化到極致的事件發布訂閱的工具庫,通過梳理可知曉:

 

  • call 與 apply 在效率上的差異
  • 對象和對象數組的存取性能考慮
  • 理解發布訂閱模式,以及在事件系統中的應用實例

 

責任編輯:武曉燕 來源: DYBOY
相關推薦

2022-04-29 08:17:38

RPC遠程代理代理模式

2021-11-08 11:32:01

觀察

2020-11-06 07:11:40

內存虛擬Redis

2020-07-28 00:58:20

IP地址子網TCP

2015-08-13 10:29:12

面試面試官

2021-02-28 07:52:24

蠕蟲數據金絲雀

2021-01-14 05:23:32

高并發消息中間件

2020-12-09 05:18:17

面試觀察者訂閱模式

2020-07-03 07:39:45

查詢語句

2020-06-17 21:22:56

Serverless面試官架構

2023-07-13 08:19:30

HaspMapRedis元素

2024-08-16 13:59:00

2021-06-29 11:05:25

MySQLCPU數據庫

2020-10-15 06:26:24

高并發場景冰河

2020-09-07 06:28:37

Nginx靜態負載均衡動態負載均衡

2020-11-02 07:02:10

加載鏈接初始化

2023-07-11 08:50:34

2021-10-29 09:40:21

設計模式軟件

2024-08-23 11:51:39

2019-05-14 08:44:13

面試面試官線程安全
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲激情网站 | 欧美成人精品激情在线观看 | 欧美日本一区 | 日韩电影中文字幕 | 国产一区91精品张津瑜 | 涩涩视频网站在线观看 | 91色站| 日韩欧美一区二区三区免费观看 | 无毛av | 国产一区视频在线 | 国产在线视频一区二区 | 毛片综合| 欧美国产精品 | 亚洲综合电影 | 懂色av一区二区三区在线播放 | 日韩三 | 91精品国产91久久久久久密臀 | 91麻豆精品国产91久久久更新资源速度超快 | 亚洲性人人天天夜夜摸 | 99re在线视频 | 国产专区免费 | 国产不卡一区 | 日韩在线不卡视频 | 日韩中文字幕在线观看视频 | 一区二区三区四区在线免费观看 | 中文成人在线 | 国产精品一区在线观看 | 精品一区二区三区四区外站 | 久久国产精品视频观看 | 毛片黄片免费看 | 日韩欧美精品一区 | 中文字幕丁香5月 | 国产成在线观看免费视频 | 91在线播 | 国产精品xxxx | 亚洲一区网站 | 91原创视频 | 黄色片网站国产 | 亚洲成人免费 | 日韩欧美第一页 | 黄色一级免费 |