擼一個JS的事件管理

_潤物無聲_發表於2018-07-26

基於js的事件管理(訂閱-釋出) --- event-mange

關於事件

在我們使用javascript開發時,我們會經常用到很多事件,如點選、鍵盤、滑鼠等等,這些物理性的事件。而我們今天所說的我稱之為事件的,是另一種形式的事件,訂閱---釋出,又叫做觀察者模式,他定義了一對多的依賴關係,當一個物件狀態發生改變時,所有依賴於它的物件都會收到通知,而在javascript中,一般習慣性的用事件模型來替代釋出---訂閱模式。

列舉一個生活中的例子來幫助大家理解這一種模式。炎熱的夏天,媽媽燒好了飯盛上桌,冒著熱氣,這時媽媽喊小明吃飯(小明在旁邊的屋子裡餓著肚子大吉大利晚上吃雞...),小明出來一看,跟媽媽說,等一會 ‘飯涼了’ 再叫我,太燙了...十分鐘後...媽媽喊你 ‘飯涼了’,快來吃飯,而這時小明聽到了媽媽的喊話說 ‘飯涼了’,便快速的出來吃完了。這個例子,就是以上介紹的訂閱---釋出模式。例子中的小明就是訂閱者(訂閱的是 ‘飯涼了’),而媽媽則是釋出者(將訊號 ‘飯涼了’ 釋出出去)。

使用訂閱---釋出模式的有著顯而易見的優點:訂閱者不用每時每刻都詢問釋出者飯是否涼了,在合適的事件點,釋出者會通知這些訂閱者,告訴他們飯涼了,他們可以過來吃了。這樣就不用把小明和媽媽強耦合在一起,當小明的弟弟妹妹都想在飯涼了在吃飯,只需告訴媽媽一聲。就像每個看官肯定都接觸過的一種訂閱---釋出:DOM事件的繫結

document.body.addEventListener('click', function (e) {
     console.log('我執行了...')
}, false)
複製程式碼

迴歸正題:

event-mange 通過訂閱-釋出模式實現的

一步一步的實現

event-mange 模組的主要方法

  • on:訂閱者,新增事件
  • emit:釋出者, 出發事件
  • once: 訂閱者,新增只能監聽一次之後就失效的事件
  • removeListener:刪除單個訂閱(事件)
  • removeAllListener: 刪除單個事件型別的訂閱或刪除全部訂閱
  • getListenerCount:獲得訂閱者的數量

event-mange 模組的主要屬性

  • MaxEventListNum: 設定單個事件最多訂閱者數量(預設為10)

基本骨架

首先,我們希望通過 event.on , event.emit 來訂閱和釋出,通過建構函式來建立一個event例項,而on,emit分別為這個例項的兩個方法, 同樣的,以上列出的所有主要方法,都是event的物件的原型方法。

function events () {};

// 列舉去我們想要實現的event物件的方法

event.prototype.on = function () {};

event.prototype.emit = function () {};

event.prototype.once = function () {};

event.prototype.removeListener = function () {};

event.prototype.removeAllListener = function () {};

event.prototype.getListenerCount = function () {};
複製程式碼

似乎丟了什麼,沒錯,是event物件我們上面列出來的MaxEventListNum屬性,我們給他補上

function event () {
    //因為MaxEventListNum屬性是可以讓開發者設定的
    //所以在沒有set的時候,我們將其設定為 undefind
    this.MaxEventListNum = this.MaxEventListNum || undefined;

    //如果沒有設定set,我們不能讓監聽數量無限大
    //這樣有可能會造成記憶體溢位
    //所以我們將預設數量設定為10(當然,設定成別的數量也是可以的)
    this.defaultMaxEventListNum = 10;
}

複製程式碼

到這裡,基本上我們想實現的時間管理模組屬性和方法的初態也就差不多了,也就是說,骨架出來了,我們就需要填飽他的程式碼邏輯,讓他變的有血有肉(看似像個生命...)

值得思考的是,骨架我們構建完了,我們要做的是一個訂閱--釋出模式,我們應該怎麼去記住眾多的訂閱事件呢? 首先,對於一個訂閱,我們需要有一個訂閱的型別,也就是topic,針對此topic我們要把所有的訂閱此topic的事件都放在一起,對,可以選擇Array,初步的構造

event_list: {
    topic1: [fn1, fn2, fn3 ...]
    ...
}
複製程式碼

那麼接下來我們將存放我們事件的event_list放入程式碼中完善,作為event的屬性

function event () {
    // 這裡我們做一個簡單的判斷,以免一些意外的錯誤出現
    if(!this.event_list) {
        this.event_list = {};
    }

    this.MaxEventListNum = this.MaxEventListNum || undefined;
    this.defaultMaxEventListNum = 10;
}
複製程式碼

on 方法實現
event.prototype.on = function () {};
複製程式碼

通過分析得出on方法首先應該接收一個訂閱的topic,其次是一個當此topic響應後觸發的callback方法

event.prototype.on = function (eventName, content) {};
複製程式碼

eventName作為事件型別,將其作為event_list的一個屬性,所有的事件型別為eventName的監聽都push到eventName這個陣列裡面。

event.prototype.on = function (eventName, content) {
    ...
    var _event, ctx;
    _event = this.event_list;
    // 再次判斷event_list是否存在,不存在則重新賦值
    if (!_event) {
      _event = this.event_list = {};
    } else {
      // 獲取當前eventName的監聽
      ctx = this.event_list[eventName];
    }
    // 判斷是否有此監聽型別
    // 如果不存在,則表示此事件第一次被監聽
    // 將回撥函式 content 直接賦值
    if (!ctx) {
      ctx = this.event_list[eventName] = content;
      // 改變訂閱者數量
      ctx.ListenerCount = 1;
    } else if (isFunction(ctx)) {
      // 判斷此屬性是否為函式(是函式則表示已經有且只有一個訂閱者)
      // 將此eventName型別由函式轉變為陣列
      ctx = this.event_list[eventName] = [ctx, content];
      // 此時訂閱者數量變為陣列長度
      ctx.ListenerCount = ctx.length;
    } else if (isArray(ctx)) {
      // 判斷是否為陣列,如果是陣列則直接push
      ctx.push(content);
      ctx.ListenerCount = ctx.length;
    }
    ...
};
複製程式碼
once 方法實現
event.prototype.once = function () {};
複製程式碼

once方法對已訂閱事件只執行一次,需執行完後立即在event_list中相應的訂閱型別屬性中刪除該訂閱的回撥函式,其儲存過程與on方法幾乎一致,同樣需要一個訂閱型別的topic,以及一個響應事件的回撥 content

event.prototype.once = function (eventName, content) {};
複製程式碼

在執行完本次事件回撥後立即取消註冊此訂閱,而如果此時同一型別的事件註冊了多個監聽回撥,我們無法準確的刪除當前once方法所註冊的監聽回撥,所以通常我們採用的遍歷事件監聽佇列,找到相應的監聽回撥然後將其刪除是行不通的。還好,偉大的javascript語言為我們提供了一個強大的閉包特性,通過閉包的方式來裝飾content,包裝成一個全新的函式。

events.prototype.once = function (event, content) {
    ...
    // once和on的儲存事件回撥機制相同
    // dealOnce 函式 包裝函式
    this.on(event, dealOnce(this, event, content));
    ...
  }

// 包裝函式
function dealOnce(target, type, content) {
    var flag = false;
    // 通過閉包特性(會將函式外部引用儲存在作用域中)
    function packageFun() {
      // 當此監聽回撥被呼叫時,會先刪除此回撥方法
      this.removeListener(type, packageFun);
      if (!flag) {
        flag = true;
        // 因為閉包,所以原監聽回撥還會保留,所以還會執行
        content.apply(target, arguments);
      }
      packageFun.content = content;
    }
    return packageFun;
  }
複製程式碼

once的實現其實將我們自己傳遞的回撥函式做了二次封裝,再繫結上封裝後的函式,封裝的函式首先執行了removeListener()移除了回撥函式與事件的繫結,然後才執行的回撥函式

emit 方法實現
event.prototype.emit = function () {};
複製程式碼

emit方法用來發布事件,驅動執行相應的事件監聽佇列中的監聽回撥,故我們需要一個事件type的topic

event.prototype.emit = function (eventName[,message][,message1][,...]) {};
複製程式碼

當然,釋出事件是,也可以像該事件監聽者傳遞引數,數量不限,則會依次傳遞給所有的監聽回撥

event.prototype.emit = function (eventName[,message]) {
    var _event, ctx;
    //除第一個引數eventNmae外,其他引數儲存在一個陣列裡
    var args = Array.prototype.slice.call(arguments, 1);
    _event = this.event_list;
    // 檢測儲存事件佇列是否存在
    if (_event) {
      // 如果存在,得到此監聽型別
      ctx = this.event_list[eventName];
    }
    // 檢測此監聽型別的事件佇列
    // 不存在則直接返回
    if (!ctx) {
      return false;
    } else if (isFunction(ctx)) {
      // 是番薯則直接執行,並將所有引數傳遞給此函式(回撥函式)
      ctx.apply(this, args);
    } else if (isArray(ctx)) {
      // 是陣列則遍歷呼叫
      for (var i = 0; i < ctx.length; i++) {
        ctx[i].apply(this, args);
      }
    }
};
複製程式碼

emit從理解程度上來說應該是更容易一些,只是從儲存事件的物件中找到相應型別的監聽事件佇列,然後執行佇列中的每一個回撥

removeListener 方法實現
event.prototype.removeListener = function () {};
複製程式碼

刪除某種監聽型別的某一個監聽回撥,顯然,我們仍然需要一個事件type,以及一個監聽回撥,當事件對列中的回撥與該回撥相同時,則移除

event.prototype.removeListener = function (eventName, content) {};
複製程式碼

需要注意的是,如果我們確實存在要移除某個監聽事件的回撥,在on方法時一定不要使用匿名函式作為回撥,這樣會導致在removeListener是無法移除,因為在javascript中匿名函式是不相等的。

// 如果需要移除

// 錯誤
event.on('eatting', function (msg) {

});

// 正確
event.on('eatting', cb);
// 回撥
function cb (msg) {
    ...
}
複製程式碼
event.prototype.removeListener = function (eventName, content) {
    var _event, ctx, index = 0;
    _event = this.event_list;
    if (!_event) {
      return this;
    } else {
      ctx = this.event_list[eventName];
    }
    if (!ctx) {
      return this;
    }
    // 如果是函式  直接delete
    if (isFunction(ctx)) {
      if (ctx === content) {
        delete _event[eventName];
      }
    } else if (isArray(ctx)) {
      // 如果是陣列 遍歷
      for (var i = 0; i < ctx.length; i++) {
        if (ctx[i] === content) {
          // 監聽回撥相等
          // 從該監聽回撥的index開始,後面的回撥依次覆蓋掉前面的回撥
          // 將最後的回撥刪除
          // 等價於直接將滿足條件的監聽回撥刪除
          this.event_list[eventName].splice(i - index, 1);
          ctx.ListenerCount = ctx.length;
          if (this.event_list[eventName].length === 0) {
            delete this.event_list[eventName]
          }
          index++;
        }
      }
    }
};

複製程式碼
removeAllListener 方法實現
event.prototype.removeAllListener = function () {};
複製程式碼

此方法有兩個用途,即實現當有引數事件型別eventName時,則刪除該型別的所有監聽(清空此事件的監聽回撥佇列),當沒有引數時,則將所有型別的事件監聽對壘全部移除,還是比較好理解的直接上程式碼

event.prototype.removeAllListener = function ([,eventName]) {
    var _event, ctx;
    _event = this.event_list;
    if (!_event) {
      return this;
    }
    ctx = this.event_list[eventName];
    // 判斷是否有引數
    if (arguments.length === 0 && (!eventName)) {
      // 無引數
      // 將key 轉成 陣列  並遍歷
      // 依次刪除所有的型別監聽
      var keys = Object.keys(this.event_list);
      for (var i = 0, key; i < keys.length; i++) {
        key = keys[i];
        delete this.event_list[key];
      }
    }
    // 有引數 直接移除
    if (ctx || isFunction(ctx) || isArray(ctx)) {
      delete this.event_list[eventName];
    } else {
      return this;
    }
};
複製程式碼

其主要實現思路大致如上所述,貌似還漏了一些什麼,哦,是對於是否超過艦艇數量的最大限制的處理 在on方法中

...
// 檢測回撥佇列是否有maxed屬性以及是否為false
if (!ctx.maxed) {
      //只有在是陣列的情況下才會做比較
      if (isArray(ctx)) {
        var len = ctx.length;
        if (len > (this.MaxEventListNum ? this.MaxEventListNum : this.defaultMaxEventListNum)) { 
        // 當超過最大限制,則會發除警告
          ctx.maxed = true;
          console.warn('events.MaxEventListNum || [ MaxEventListNum ] :The number of subscriptions exceeds the maximum, and if you do not set it, the default value is 10');
        } else {
          ctx.maxed = false;
        }
      }
    }

...
複製程式碼

現在Vue可謂是紅的發紫,沒關係,events-manage也可以在Vue中掛在到全域性使用哦

events.prototype.install = function (Vue, Option) {
    Vue.prototype.$ev = this;
  }
複製程式碼

不用多解釋了吧,想必看官都明白應該怎麼使用了吧(在Vue中)

關於本庫更具體更詳細的使用文件,趕緊戳這裡

碼字不易啊,如果覺得對您有一些幫助,還請給一個大大的贊?哈哈

(...已是凌晨...)

相關文章