基於JS的event-manage事件管理庫(一步一步實現)

關於文章

最近在提高我的技能的同時,決定把本身爲數很少的沉澱記錄下來,讓本身理解的更加深入,同時也歡迎各位看官指出不足之處。javascript

隨着node.js的盛行,引領着Javascript上天下地無所不能啊,本人確確實實的一個前端的忠實粉絲,因而乎......不能自拔...node的異步回調機制有效的提升了非密集型程序的高併發、速度快、性能優的需求同時,也飽受各大廠商的青睞,正是node將javascript又一次推向了熱潮,因而小白又要埋頭苦讀,在各位大神和看官指引的道路下前進。前端

這篇文章是個人第一篇文章,很早就下定決心要記錄和分享一些本身對已掌握的知識的一些理解(其實真心以爲將本身學到東西以文章的形式分享給你們,話風通俗易懂,不只鞏固了本身的知識體系,還會 潤物無聲),可遲遲沒有勇氣和信心。java

這篇文章主要記錄一下本身前幾天手寫的一個兼容node環境、瀏覽器環境、還支持Vue的...庫。event-mange( npm倉庫的名字)。node下的EventEmitter想必你們都很熟悉了,在思考事後決定本身也要產出一個相似的事件管理,不只能在node下,還能夠在瀏覽器端以 script標籤 的方式引入使用,還有CMD、AMD下...因而,本小白開始動工了。node

關於事件

在咱們使用javascript開發時,咱們會常常用到不少事件,如點擊、鍵盤、鼠標等等,這些物理性的事件。而咱們今天所說的我稱之爲事件的,是另外一種形式的事件,訂閱---發佈,又叫作觀察者模式,他定義了一對多的依賴關係,當一個對象狀態發生改變時,全部依賴於它的對象都會收到通知,而在javascript中,通常習慣性的用事件模型來替代發佈---訂閱模式。git

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

使用訂閱---發佈模式的有着顯而易見的優勢:訂閱者不用每時每刻都詢問發佈者飯是否涼了,在合適的事件點,發佈者會通知這些訂閱者,告訴他們飯涼了,他們能夠過來吃了。這樣就不用把小明和媽媽強耦合在一塊兒,當小明的弟弟妹妹都想在飯涼了在吃飯,只需告訴媽媽一聲。就像每一個看官確定都接觸過的一種訂閱---發佈:DOM事件的綁定npm

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中)

關於本庫更具體更詳細的使用文檔,趕忙戳這裏

碼字不易啊,若是以爲對您有一些幫助,還請給一個大大的贊👍哈哈

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

相關文章
相關標籤/搜索