<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          發(fā)布訂閱設(shè)計(jì)模式

          共 16641字,需瀏覽 34分鐘

           ·

          2021-08-10 16:32

          發(fā)布訂閱設(shè)計(jì)模式在程序中經(jīng)常涉及,例如 Vue 中的 $on$offdocument.addEventListener()document.removeEventListener()等,發(fā)布訂閱模式可以降低程序的耦合度,統(tǒng)一管理維護(hù)消息、處理事件也使得程序更容易維護(hù)和擴(kuò)展。

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

          最近在程序中使用到了 eventEmitter3 這個(gè)事件發(fā)布訂閱庫,該庫可用于組件之間的通信管理,通過簡(jiǎn)單的 Readme 文檔可學(xué)會(huì)如何使用,但同時(shí)了解這個(gè)庫的設(shè)計(jì)也有助于大家了解認(rèn)識(shí)發(fā)布訂閱設(shè)計(jì)模式,不妨一起來看看。

          一、定義

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

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

          放到程序的組件中,多個(gè)組件的通信除了父子組件傳值外,還有例如 reduxvuex 狀態(tài)管理,另外就是本文所說的發(fā)布訂閱模式,可以通過一個(gè)事件中心來實(shí)現(xiàn)。

          發(fā)布訂閱模式

          二、手搓一個(gè)發(fā)布訂閱事件中心

          “紙上得來終覺淺,絕知此事要躬行”,所以根據(jù)定義,我們嘗試實(shí)現(xiàn)一個(gè)JavaScript版本的發(fā)布訂閱事件中心,看看會(huì)遇到哪些問題?

          2.1 基本結(jié)構(gòu)版

          首先實(shí)現(xiàn)的 DiyEventEmitter 如下:

          /**
           * 事件發(fā)布訂閱中心
           */

          class DiyEventEmitter {
            static instance: DiyEventEmitter;
            private _eventsMap: Map<stringArray<() => void>>;

            static getInstance() {
              if (!DiyEventEmitter.instance) {
                DiyEventEmitter.instance = new DiyEventEmitter();
              }
              return DiyEventEmitter.instance;
            }

            constructor() {
              this._eventsMap = new Map(); // 事件名與回調(diào)函數(shù)的映射Map
            }

            /**
             * 事件訂閱
             *
             * @param eventName 事件名
             * @param eventFnCallback 事件發(fā)生時(shí)的回調(diào)函數(shù)
             */

            public on(eventName: string, eventFnCallback: () => void) {
              const newArr = this._eventsMap.get(eventName) || [];
              newArr.push(eventFnCallback);
              this._eventsMap.set(eventName, newArr);
            }

            /**
             * 取消訂閱
             *
             * @param eventName 事件名
             * @param eventFnCallback 事件發(fā)生時(shí)的回調(diào)函數(shù)
             */

            public off(eventName: string, eventFnCallback?: () => void) {
              if (!eventFnCallback) {
                this._eventsMap.delete(eventName);
                return;
              }

              const newArr = this._eventsMap.get(eventName) || [];
              for (let i = newArr.length - 1; i >= 0; i--) {
                if (newArr[i] === eventFnCallback) {
                  newArr.splice(i, 1);
                }
              }
              this._eventsMap.set(eventName, newArr);
            }

            /**
             * 主動(dòng)通知并執(zhí)行注冊(cè)的回調(diào)函數(shù)
             *
             * @param eventName 事件名
             */

            public emit(eventName: string) {
              const fns = this._eventsMap.get(eventName) || [];
              fns.forEach(fn => fn());
            }
          }

          export default DiyEventEmitter.getInstance();

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

          import e from "./DiyEventEmitter";

          const subscribeFn = () => {
            console.log("DYBOY訂閱收到了消息");
          };
          const subscribeFn2 = () => {
            console.log("DYBOY第二個(gè)訂閱收到了消息");
          };

          // 訂閱
          e.on("dyboy", subscribeFn);
          e.on("dyboy", subscribeFn2);

          // 發(fā)布消息
          e.emit("dyboy");

          // 取消第一個(gè)訂閱消息的綁定
          e.off("dyboy", subscribeFn);

          // 第二次發(fā)布消息
          e.emit("dyboy");

          輸出 console 結(jié)果:

          DYBOY訂閱收到了消息
          第二個(gè)訂閱的消息
          第二個(gè)訂閱的消息

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

          2.2 支持只訂閱一次once方法

          在一些場(chǎng)景下,某些事件訂閱可能只需要執(zhí)行一次,后續(xù)的通知將不再響應(yīng)。

          實(shí)現(xiàn)的思路:新增 once 訂閱方法,當(dāng)響應(yīng)了對(duì)應(yīng)“發(fā)布者消息”,則主動(dòng)取消訂閱當(dāng)前執(zhí)行的回調(diào)函數(shù)。

          為此新增類型,如此便于回調(diào)函數(shù)的描述信息擴(kuò)展:

          type SingleEvent = {
            fn: () => void;
            once: boolean;
          };

          _eventsMap的類型更改為:

          private _eventsMap: Map<stringArray<SingleEvent>>;

          同時(shí)抽出公共方法 addListener,供 ononce 方法共用:

          private addListener( eventName: string, eventFnCallback: () => void, once = false) {
            const newArr = this._eventsMap.get(eventName) || [];
            newArr.push({
              fn: eventFnCallback,
              once,
            });
            this._eventsMap.set(eventName, newArr);
          }

          /**
           * 事件訂閱
           *
           * @param eventName 事件名
           * @param eventFnCallback 事件發(fā)生時(shí)的回調(diào)函數(shù)
           */

          public on(eventName: string, eventFnCallback: () => void) {
            this.addListener(eventName, eventFnCallback);
          }

          /**
           * 事件訂閱一次
           *
           * @param eventName 事件名
           * @param eventFnCallback 事件發(fā)生時(shí)的回調(diào)函數(shù)
           */

          public once(eventName: string, eventFnCallback: () => void) {
            this.addListener(eventName, eventFnCallback, true);
          }

          與此同時(shí),我們需要考慮在觸發(fā)事件時(shí)候,執(zhí)行一次就需要取消訂閱

          /**
           * 觸發(fā):主動(dòng)通知并執(zhí)行注冊(cè)的回調(diào)函數(shù)
           *
           * @param eventName 事件名
           */

          public emit(eventName: string) {
            const fns = this._eventsMap.get(eventName) || [];
            fns.forEach((evt, index) => {
              evt.fn();
              if (evt.once) fns.splice(index, 1);
            });
            this._eventsMap.set(eventName, fns);
          }

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

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

          2.3 緩存發(fā)布消息

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

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

          UML時(shí)序圖

          發(fā)布者發(fā)布消息,事件中心檢測(cè)是否存在訂閱者,如果沒有訂閱者訂閱此條消息,則把該消息緩存到離線消息隊(duì)列中,當(dāng)有訂閱者訂閱時(shí),檢測(cè)是否訂閱了緩存中的事件消息,如果是,則該事件的緩存消息依次出隊(duì)(FCFS調(diào)度執(zhí)行),觸發(fā)訂閱者回調(diào)函數(shù)執(zhí)行一次。

          新增離線消息緩存隊(duì)列:

          private _offlineMessageQueue: Map<stringnumber>;

          在emit發(fā)布消息中判斷對(duì)應(yīng)事件是否有訂閱者,沒有訂閱者則向離線事件消息中更新

          /**
           * 觸發(fā):主動(dòng)通知并執(zhí)行注冊(cè)的回調(diào)函數(shù)
           *
           * @param eventName 事件名
           */
          public emit(eventName: string) {
            const fns = this._eventsMap.get(eventName) || [];
          +  if (fns.length === 0) {
          +    const counter = this._offlineMessageQueue.get(eventName) || 0;
          +    this._offlineMessageQueue.set(eventName, counter + 1);
          +    return;
          +  }
            fns.forEach((evt, index) => {
              evt.fn();
              if (evt.once) fns.splice(index, 1);
            });
            this._eventsMap.set(eventName, fns);
          }

          然后在 addListener 方法中根據(jù)離線事件消息統(tǒng)計(jì)的次數(shù),重新emit發(fā)布事件消息,觸發(fā)消息回調(diào)函數(shù)執(zhí)行,之后刪掉離線消息中的對(duì)應(yīng)事件。

          private addListener(
            eventName: string,
            eventFnCallback: () => void,
            once = false
          ) {
            const newArr = this._eventsMap.get(eventName) || [];
            newArr.push({
              fn: eventFnCallback,
              once,
            });
            this._eventsMap.set(eventName, newArr);

          +  const cacheMessageCounter = this._offlineMessageQueue.get(eventName);
          +  if (cacheMessageCounter) {
          +    for (let i = 0; i < cacheMessageCounter; i++) {
          +      this.emit(eventName);
          +    }
          +    this._offlineMessageQueue.delete(eventName);
          +  }
          }

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

          2.4 回調(diào)函數(shù)傳參&執(zhí)行環(huán)境

          在上面的回調(diào)函數(shù)中,我們可以發(fā)現(xiàn)是一個(gè)沒有返回值,沒有入?yún)⒌暮瘮?shù),這其實(shí)有些雞肋,在函數(shù)運(yùn)行的時(shí)候會(huì)指向執(zhí)行的上下文,可能某些回調(diào)函數(shù)中含有this指向就無法綁定到事件中心上,因此針對(duì)回調(diào)函數(shù)需要綁定執(zhí)行上下文環(huán)境。

          2.4.1 支持回調(diào)函數(shù)傳參

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

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

          另外如果真要支持回調(diào)函數(shù)傳參,那么就需要在 emit() 的時(shí)候傳入?yún)?shù),然后再將參數(shù)傳遞給回調(diào)函數(shù),這里我們暫時(shí)先不實(shí)現(xiàn)了。

          2.4.2 執(zhí)行環(huán)境綁定

          在需要實(shí)現(xiàn)執(zhí)行環(huán)境綁定這個(gè)功能前,先思考一個(gè)問題:“是應(yīng)該開發(fā)者自行綁定還是應(yīng)該事件中心來做?”

          換句話說,開發(fā)者在 on('eventName', 回調(diào)函數(shù)) 的時(shí)候,是否應(yīng)該主動(dòng)綁定 this 指向?在當(dāng)前設(shè)計(jì)下,初步認(rèn)為無參數(shù)的回調(diào)函數(shù)自行綁定 this 比較合適。

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

          到這里我們自己手搓簡(jiǎn)單的發(fā)布訂閱事件中心就完成了!

          三、學(xué)習(xí)EventEmitter3的設(shè)計(jì)實(shí)現(xiàn)

          雖然我們按照自己的理解實(shí)現(xiàn)了一版,但是沒有對(duì)比我們也不知道好壞,因此一起看看 EventEmitter3 這個(gè)優(yōu)秀“極致性能優(yōu)化”的庫是怎么去處理事件訂閱與發(fā)布,同時(shí)可以學(xué)習(xí)下其中的性能優(yōu)化思路。

          首先,EventEmitter3(后續(xù)簡(jiǎn)稱:EE3)的實(shí)現(xiàn)思路,用Events對(duì)象作為“回調(diào)事件對(duì)象”的存儲(chǔ)器,類比我們上述實(shí)現(xiàn)的“發(fā)布訂閱模式”作為事件的執(zhí)行邏輯,另外addListener() 函數(shù)增加了傳入執(zhí)行上下文環(huán)境參數(shù),emit() 函數(shù)支持最多傳入5個(gè)參數(shù),同時(shí)EventEmitter3中還加入了監(jiān)聽器計(jì)數(shù)、事件名前綴。

          3.1 Events存儲(chǔ)器

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

          在JavaScript中萬物是對(duì)象,函數(shù)也是對(duì)象,因此存儲(chǔ)器的實(shí)現(xiàn):

          function Events({}

          3.2 事件偵聽器實(shí)例

          同理,我們上述使用singleEvent對(duì)象來存儲(chǔ)每一個(gè)事件偵聽器實(shí)例,EE3 中用一個(gè)EE對(duì)象存儲(chǔ)每個(gè)事件偵聽器的實(shí)例以及必要屬性

          /**
           * 每個(gè)事件偵聽器實(shí)例的表示形式
           *
           * @param {Function} fn 偵聽器函數(shù)
           * @param {*} context 調(diào)用偵聽器的執(zhí)行上下文
           * @param {Boolean} [once=false] 指定偵聽器是否僅支持調(diào)用一次
           * @constructor
           * @private
           */

          function EE(fn, context, once{
            this.fn = fn;
            this.context = context;
            this.once = once || false;
          }

          3.3 添加偵聽器方法

          /**
           * 為給定事件添加偵聽器
           *
           * @param {EventEmitter} emitter EventEmitter實(shí)例的引用.
           * @param {(String|Symbol)} event 事件名.
           * @param {Function} fn 偵聽器函數(shù).
           * @param {*} context 調(diào)用偵聽器的上下文.
           * @param {Boolean} once 指定偵聽器是否僅支持調(diào)用一次.
           * @returns {EventEmitter}
           * @private
           */

          function addListener(emitter, event, fn, context, once{
            if (typeof fn !== 'function') {
              throw new TypeError('The listener must be a function');
            }

            var listener = new EE(fn, context || emitter, once)
              , evt = prefix ? prefix + event : event;

            // TODO: 這里為什么先是使用對(duì)象,多個(gè)的時(shí)候使用對(duì)象數(shù)組存儲(chǔ),有什么好處?
            if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
            else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
            else emitter._events[evt] = [emitter._events[evt], listener];

            return emitter;
          }

          該“添加偵聽器”的方法有幾個(gè)關(guān)鍵功能點(diǎn):

          1. 如果有前綴,給事件名增加前綴,避免事件沖突
          2. 每次新增事件名則 _eventsCount+1,用于快速讀寫所有事件的數(shù)量
          3. 如果事件只有單個(gè)偵聽器,則 _events[evt] 指向這個(gè) EE 對(duì)象,訪問效率更高

          3.4 清除事件

          /**
           * 通過事件名清除事件
           *
           * @param {EventEmitter} emitter EventEmitter實(shí)例的引用
           * @param {(String|Symbol)} evt 事件名
           * @private
           */

          function clearEvent(emitter, evt{
            if (--emitter._eventsCount === 0) emitter._events = new Events();
            else delete emitter._events[evt];
          }

          清除事件,只需要使用 delete 關(guān)鍵字,刪除對(duì)象上的屬性

          另外這里一個(gè)很巧妙的地方在于,依賴事件計(jì)數(shù)器,如果計(jì)數(shù)器為0,則重新創(chuàng)建一個(gè) Events 存儲(chǔ)器指向 emitter_events 屬性。

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

          3.5 EventEmitter

          function EventEmitter({
            this._events = new Events();
            this._eventsCount = 0;
          }

          EventEmitter 對(duì)象參考 NodeJS 中的事件觸發(fā)器,定義了最小的接口模型,包含 _events_eventsCount屬性,另外的方法都通過原型來增加。

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

          EventEmitter

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

          emit() 方法實(shí)現(xiàn)如下:

          /**
           * 調(diào)用執(zhí)行指定事件名的每一個(gè)偵聽器
           *
           * @param {(String|Symbol)} event 事件名.
           * @returns {Boolean} `true` 如果當(dāng)前事件名沒綁定偵聽器,則返回false.
           * @public
           */

          EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5{
            var evt = prefix ? prefix + event : event;

            if (!this._events[evt]) return false;

            var listeners = this._events[evt]
              , len = arguments.length
              , args
              , i;

            // 如果只有一個(gè)偵聽器綁定了該事件名
            if (listeners.fn) {
              // 如果是執(zhí)行一次的,則移除偵聽器
              if (listeners.once) this.removeListener(event, listeners.fn, undefinedtrue);
              
              // Refrence:https://juejin.cn/post/6844903496450310157
              // 這里的處理是從性能上考慮,傳入5個(gè)入?yún)?shù)的調(diào)用call方法處理
              // 超過5個(gè)參數(shù)的使用apply處理
              // 大部分場(chǎng)景超過5個(gè)參數(shù)的都是少數(shù)
              switch (len) {
                case 1return listeners.fn.call(listeners.context), true;
                case 2return listeners.fn.call(listeners.context, a1), true;
                case 3return listeners.fn.call(listeners.context, a1, a2), true;
                case 4return listeners.fn.call(listeners.context, a1, a2, a3), true;
                case 5return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
                case 6return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
              }

              for (i = 1, args = new Array(len -1); i < len; i++) {
                args[i - 1] = arguments[i];
              }

              listeners.fn.apply(listeners.context, args);
            } else {
              // 當(dāng)有多個(gè)偵聽器綁定了同一個(gè)事件名
              var length = listeners.length
                , j;
              
              // 循環(huán)執(zhí)行每一個(gè)綁定的事件偵聽器
              for (i = 0; i < length; i++) {
                if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefinedtrue);

                switch (len) {
                  case 1: listeners[i].fn.call(listeners[i].context); break;
                  case 2: listeners[i].fn.call(listeners[i].context, a1); break;
                  case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
                  case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
                  default:
                    if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
                      args[j - 1] = arguments[j];
                    }
                    listeners[i].fn.apply(listeners[i].context, args);
                }
              }
            }

            return true;
          };

          emit() 方法中顯示的傳入了五個(gè)入?yún)ⅲ?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(40, 202, 113);">a1 ~ a5,同時(shí)優(yōu)先使用 call() 方法綁定 this 指向并執(zhí)行偵聽器的回調(diào)函數(shù)。

          這樣處理的原因是,call 方法比 apply 方法效率更高,相關(guān)比較驗(yàn)證討論可參考《call和apply的性能對(duì)比》

          到這基本上 EventEmitter3 的實(shí)現(xiàn)就啃完了!

          四、總結(jié)

          EventEmitter3 是一個(gè)號(hào)稱優(yōu)化到極致的事件發(fā)布訂閱的工具庫,通過梳理可知曉:

          1. call 與 apply 在效率上的差異
          2. 對(duì)象和對(duì)象數(shù)組的存取性能考慮
          3. 理解發(fā)布訂閱模式,以及在事件系統(tǒng)中的應(yīng)用實(shí)例


          瀏覽 110
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  无码在线免费播放 | 亚洲视频在线观看视频 | 15—17女人毛片 18日本xxxxx | 亚洲精品午夜在线 | 青青操娱乐日韩 |