按部就班教你實現一個完整的node的EventEmitter模塊

node的事件模塊只包含了一個類:EventEmitter。這個類在node的內置模塊和第三方模塊中大量使用。EventEmitter本質上是一個觀察者模式的實現,這種模式能夠擴展node在多個進程或網絡中運行。本文從node的EventEmitter的使用出發,按部就班的實現一個完整的EventEmitter模塊。node

  • EventEmitter模塊的基本用法和簡單實現
  • node中經常使用的EventEmitter模塊的API
  • EventEmitter模塊的異常處理
  • 完整的實現一個EventEmitter模塊

原文地址:https://github.com/forthealll...git

若是文章對您有幫助,您的star是對我最好的鼓勵~github

1、EventEmitter模塊的基本用法和簡單實現

(1) EventEmitter模塊的基本用法

首先先了解一下EventEmitter模塊的基本用法,EventEmitter本質上是一個觀察者模式的實現,所謂觀察者模式:數組

它定義了對象間的一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,全部依賴於它的對象都將獲得通知。網絡

所以最基本的EventEmitter功能,包含了一個觀察者和一個被監聽的對象,對應的實現就是EventEmitter中的on和emit:app

var events=require('events');
var eventEmitter=new events.EventEmitter();
eventEmitter.on('say',function(name){
    console.log('Hello',name);
})
eventEmitter.emit('say','Jony yu');

eventEmitter是EventEmitter模塊的一個實例,eventEmitter的emit方法,發出say事件,經過eventEmitter的on方法監聽,從而執行相應的函數。dom

(2) 簡單實現一個EventEmitter模塊

根據上述的例子,咱們知道了EventEmitter模塊的基礎功能emit和on。下面咱們實現一個包含emit和on方法的EventEmitter類。異步

on(eventName,callback)方法傳入兩個參數,一個是事件名(eventName),另外一個是相應的回調函數,咱們選擇在on的時候針對事件名添加監聽函數,用對象來包含全部事件。在這個對象中對象名錶示事件名(eventName),而對象的值是一個數組,表示該事件名所對應的執行函數。函數

emit(eventName,...arg)方法傳入的參數,第一個爲事件名,其餘參數事件對應的執行函數中的實參,emit方法的功能就是從事件對象中,尋找對應key爲eventName的屬性,執行該屬性所對應的數組裏面每個執行函數。ui

下面來實現一個EventEmitter類

class EventEmitter{
   constructor(){
      this.handler={};
   }
   on(eventName,callback){
      if(!this.handles){
        this.handles={};
      }
      if(!this.handles[eventName]){
        this.handles[eventName]=[];
      }
      this.handles[eventName].push(callback);
   }
   emit(eventName,...arg){
       if(this.handles[eventName]){
     for(var i=0;i<this.handles[eventName].length;i++){
       this.handles[eventName][i](...arg);
     }
   }
   
   }
}

上述就實現了一個簡單的EventEmitter類,下面來實例化:

let event=new EventEmitter();
event.on('say',function(str){
   console.log(str);
});
event.emit('say','hello Jony yu');
//輸出hello Jony yu

2、node中經常使用的EventEmitter模塊的API

跟在上述簡單的EventEmitter模塊不一樣,node的EventEmitter還包含了不少經常使用的API,咱們一一來介紹幾個實用的API.

方法名 方法描述
addListener(event, listener) 爲指定事件添加一個監聽器到監聽器數組的尾部。
prependListener(event,listener) 與addListener相對,爲指定事件添加一個監聽器到監聽器數組的頭部。
on(event, listener) 其實就是addListener的別名
once(event, listener) 爲指定事件註冊一個單次監聽器,即 監聽器最多隻會觸發一次,觸發後馬上解除該監聽器。
removeListener(event, listener) 移除指定事件的某個監聽器,監聽器必須是該事件已經註冊過的監聽器
off(event, listener) removeListener的別名
removeAllListeners([event]) 移除全部事件的全部監聽器, 若是指定事件,則移除指定事件的全部監聽器。
setMaxListeners(n) 默認狀況下, EventEmitters 若是你添加的監聽器超過 10 個就會輸出警告信息。 setMaxListeners 函數用於提升監聽器的默認限制的數量。
listeners(event) 返回指定事件的監聽器數組。
emit(event, [arg1], [arg2], [...]) 按參數的順序執行每一個監聽器,若是事件有註冊監聽返回 true,不然返回 false。

除此以外,還有2個特殊的,不須要手動添加,node的EventEmitter模塊自帶的特殊事件:

事件名 事件描述
newListener 該事件在添加新事件監聽器的時候觸發
removeListener 從指定監聽器數組中刪除一個監聽器。須要注意的是,此操做將會改變處於被刪監聽器以後的那些監聽器的索引

上述node的EventEmitter的模塊看起來不少很複雜,其實上述的API中包含了一些別名,仔細整理,理解其使用和實現不是很困難,下面一一對比和介紹上述的API。

(1) addListener和removeListener、on和off方法

addListener(eventName,listener)的做用是爲指定事件添加一個監聽器. 其別名爲on

removeListener(eventName,listener)的做用是爲移除某個事件的監聽器. 其別名爲off

再次須要強調的是:addListener的別名是on,removeListener的別名是off

EventEmitter.prototype.on=EventEmitter.prototype.addListener
EventEmitter.prototype.off=EventEmitter.prototype.removeListener

接着咱們來看具體的用法:

var events=require('events');
var emitter=new events.EventEmitter();
function hello1(name){
  console.log("hello 1",name);
}
function hello2(name){
  console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.emit('say','Jony');
//輸出hello 1 Jony 
//輸出hello 2 Jony
emitter.removeListener('say',hello1);
emitter.emit('say','Jony');
//相應的監聽say事件的hello1事件被移除
//只輸出hello 2 Jony

(2) removeListener和removeAllListeners

removeListener指的是移除一個指定事件的某一個監聽器,而removeAllListeners指的是移除某一個指定事件的所有監聽器。
這裏舉例一個removeAllListeners的例子:

var events=require('events');
var emitter=new events.EventEmitter();
function hello1(name){
  console.log("hello 1",name);
}
function hello2(name){
  console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.removeAllListeners('say');
emitter.emit('say','Jony');
//removeAllListeners移除了全部關於say事件的監聽
//所以沒有任何輸出

(3) on和once方法

on和once的區別是:

on的方法對於某一指定事件添加的監聽器能夠持續不斷的監聽相應的事件,而once方法添加的監聽器,監聽一次後,就會被消除。

好比on方法(跟addListener相同):

var events=require('events');
var emitter=new events.EventEmitter();
function hello(name){
  console.log("hello",name);
}
emitter.on('say',hello);
emitter.emit('say','Jony');
emitter.emit('say','yu');
emitter.emit('say','me');
//會一次輸出 hello Jony、hello yu、hello me

也就是說on方法監聽的事件,能夠持續不斷的被觸發。

(4) 兩個特殊的事件newListener和removeListener

咱們知道當實例化EventEmitter模塊以後,監聽對象是一個對象,包含了全部的監聽事件,而這兩個特殊的方法就是針對監聽事件的添加和移除的。

newListener:在添加新事件監聽器觸發
removeListener:在移除事件監聽器時觸發

以newListener爲例,會在添加新事件監聽器的時候觸發:

var events=require('events');
var emitter=new events.EventEmitter();

function hello(name){
  console.log("hello",name);
}
emitter.on('newListener',function(eventName,listener){
  console.log(eventName);
  console.log(listener);
});
emitter.addListener('say',hello);
//輸出say和[Function: hello]

從上述的例子來看,每當添加新的事件,都會自動的emit一個「newListener」事件,且參數爲eventName(新事件的名)和listener(新事件的執行函數)。

同理特殊事件removeListener也是一樣的,當事件被移除,會自動emit一個"removeListener"事件。

3、EventEmitter模塊的異常處理

(1) node中的try catch異常處理方法

在node中也能夠經過try catch方式來捕獲和處理異常,好比:

try {
  let x=x;
} catch (e) {
  console.log(e);
}

上述let x=x 賦值語句的錯誤會被捕獲。這裏提異常處理,那麼跟事件有什麼關係呢?

node中有一個特殊的事件error,若是異常沒有被捕獲,就會觸發process的uncaughtException事件拋出,若是你沒有註冊該事件的監聽器(即該事件沒有被處理),則 Node.js 會在控制檯打印該異常的堆棧信息,並結束進程。

好比:

var events=require('events');
var emitter=new events.EventEmitter();
emitter.emit('error');

在上述代碼中沒有監聽error的事件函數,所以會觸發process的uncaughtException事件,從而打印異常堆棧信息,並結束進程。

對於阻塞或者說非異步的異常捕獲,try catch是沒有問題的,可是:

try catch不能捕獲非阻塞或者異步函數裏面的異常。

舉例來講:

try {
  let x=x;//第二個x在使用前未定義,會拋出異常
} catch (e) {
  console.log('該異常已經被捕獲');
  console.log(e);
}

上述代碼中,覺得try方法裏面是同步的,所以能夠捕獲異常。若是try方法裏面有異步的函數:

try {
  process.nextTick(function(){
      let x=x; //第二個x在使用前未定義,會拋出異常
  });
} catch (e) {
  console.log('該異常已經被捕獲');
  console.log(e);
}

由於process.nextTick是異步的,所以在process.nextTick內部的錯誤不能被捕獲,也就是說try catch不能捕獲非阻塞函數內的異常。

(2) 經過domains管理異常

node中domain模塊能被用來集中地處理多個異常操做,經過node的domain模塊能夠捕獲非阻塞函數內的異常。

var domain=require('domain');
var eventDomain=domain.create();
eventDomain.on('error',function(err){
  console.log('該異常已經被捕獲了');
  console.log(err);
});
eventDomain.run(function(){
   process.nextTick(function(){
     let x=x;//拋出異常
   });
});

一樣的,即便process.nextTick是一個異步函數,domain.on方法也能夠捕獲這個異步函數中的異常。

即便更復雜的狀況下,好比異步嵌套異步的狀況下,domain.on方法也能夠捕獲異常。

var domain=require('domain');
var eventDomain=domain.create();
eventDomain.on('error',function(err){
  console.log('該異常已經被捕獲了');
  console.log(err);
});
eventDomain.run(function(){
   process.nextTick(function(){
     setTimeout(function(){
       setTimeout(function(){
         let x=x;
       },0);
     },0);
   });
});

在上述的狀況下,即便異步嵌套很複雜,也能在最外層捕獲到異常。

(3) domain模塊缺陷

在node最新的文檔中,domain被廢除(被標記爲:Deprecated),domain從誕生之日起就有着缺陷,舉例來講:

var domain = require('domain');
var EventEmitter = require('events').EventEmitter;

var e = new EventEmitter();

var timer = setTimeout(function () {
  e.emit('data');
}, 10);

function next() {
  e.once('data', function () {
    throw new Error('something wrong here');
  });
}

var d = domain.create();
d.on('error', function () {
  console.log('cache by domain');
});

d.run(next);

如上述的代碼是沒法捕獲到異常Error的,緣由在於發出異常的EventEmitter實例e,以及觸發異常的定時函數timer沒有被domain包裹。domain模塊是經過重寫事件循環中的nextTick和_tickCallback來事件將process.domain注入到next包裹的全部異步事件內。

解決上述沒法捕獲異常的狀況,只須要將e或者timer包裹進domain。

d.add(e)或者d.add(timer)

就能夠成功的捕獲異常。

domain模塊已經在node最新的文檔中被廢除

(4)process.on('uncaughtException')的方法捕獲異常

node中提供了一個最外層的兜底的捕獲異常的方法。非阻塞或者異步函數中的異常都會拋出到最外層,若是異常沒有被捕獲,那麼會暴露出來,被最外層的process.on('uncaughtException')所捕獲。

try {
  process.nextTick(function(){
     let x=x; //第二個x在使用前未定義,會拋出異常
  },0);
} catch (e) {
  console.log('該異常已經被捕獲');
  console.log(e);
}
process.on('uncaughtException',function(err){console.log(err)})

這樣就能在最外層捕獲異步或者說非阻塞函數中的異常。

4、完整的實現一個EventEmitter模塊(可選讀)

在第二節中咱們知道了EventEmitter模塊的基本用法,那麼根據基本的API咱們能夠進一步本身去實現一個EventEmitter模塊。
每個EventEmitter實例都有一個包含全部事件的對象_events,
事件的監聽和監聽事件的觸發,以及監聽事件的移除等都在這個對象_events的基礎上實現。

(1) emit

emit的方法實現的大體功能以下程序流程圖所示:

default

從上述的程序圖出發,咱們開始實現本身的EventEmitter模塊。

首先生成一個EventEmitter類,在類的初始化方法中生成這個事件對象_events.

class EventEmitter{
  constructor(){
    if(this._events===undefined){
      this._events=Object.create(null);//定義事件對象
      this._eventsCount=0;
    }
  }
}

_eventsCount用於統計事件的個數,也就是_events對象有多少個屬性。

接着咱們來實現emit方法,根據框圖,咱們知道emit所作的事情是在_events對象中取出相應type的屬性,並執行屬性所對應的函數,咱們來實現這個emit方法。

class EventEmitter{
  constructor(){
    if(this._events===undefined){
      this._events=Object.create(null);//定義事件對象
      this._eventsCount=0;
    }
  }
  emit(type,...args){
    const events=this._events;
    const handler=events[type];
    //判斷相應type的執行函數是否爲一個函數仍是一個數組
    if(typeof handler==='function'){
      Reflect.apply(handler,this,args);
    }else{
      const len=handler.length;
      for(var i=0;li<len;i++){
       Reflect.apply(handler[i],this,args);
      }
    }
    return true;
  }
}

(2) on、addListener和prependListener方法

emit方法是出發事件,並執行相應的方法,而on方法則是對於指定的事件添加監聽函數。用程序來講,就是往事件對象中_events添加相應的屬性.程序流程圖以下所示:

2

接着咱們來實現這個方法:

on(type,listener,prepend){
    var m;
    var events;
    var existing;
    events=this._events;
    //添加事件的
    if(events.newListener!==undefined){
       this.emit('newListener',type,listener);
       events=target._events;
    }
    existing=events[type];
    //判斷相應的type的方法是否存在
    if(existing===undefined){
      //若是相應的type的方法不存在,這新增一個相應type的事件
      existing=events[type]=listener;
      ++this._eventsCount;
    }else{
      //若是存在相應的type的方法,判斷相應的type的方法是一個數組仍是僅僅只是一個方法
      //若是僅僅是
      if(typeof existing==='function'){
        //若是僅僅是一個方法,則添加
        existing=events[type]=prepend?[listener,existing]:[existing,listener];
      }else if(prepend){
        existing.unshift(listener);
      }else{
        existing.push(listener);
      }
    }
    //鏈式調用
    return this;
}
  • 在on方法中爲了能夠鏈式的調用咱們返回了EventEmitter模塊的實例化自己。
  • 且在on方法的參數中,第三個參數用於指定是在相應事件類型屬性所對應的數組頭部添加仍是尾部添加,不傳的狀況下實在尾部添加,若是指定prepend爲true,則相同事件類型的新的監聽事件會添加到事件數組的頭部。
  • 若是_events存在newListener屬性,也就是說_event存在監聽newListener監聽事件,那麼每次on方法添加事件的時候,都會emit出一個‘newListener’方法。

在on方法的基礎上能夠實現addListener方法和prependListener方法。

addListener方法是on方法的別名:

EventEmitter.prototype.addListener=EventEmitter.prototype.on

prependListener方法至關於在頭部添加,指定prepend爲true:

EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
  return EventEmitter.prototype.on(type, listener, true);
};

(3) removeListener和removeAllListeners

接着來看移除事件監聽的方法removeListener和removeAllListeners,下面咱們來看removeListener的程序流程圖:

3

接着來看removeListener的代碼:

removeListener(type,listener){
 var list,events,position,i,originalListener;
 events=this._events;
 list=events[type];
 //若是相應的事件對象的屬性值是一個函數,也就是說事件只被一個函數監聽
 if(list===listener){
    if(--this._eventsCount===0){
      this._events=Object.create(null);
    }else{
      delete events[type];
      //若是存在對移除事件removeListener的監聽函數,則觸發removeListener
      if(events.removeListener)
         this.emit('removeListener',type,listener);
    }
 }else if(typeof list!=='function'){
   //若是相應的事件對象屬性值是一個函數數組
   //遍歷這個數組,找出listener對應的那個函數,在數組中的位置
   for(i=list.length-1;i>=0;i--){
     if(list[i]===listener){
       position=i;
       break;
     }
   }
   //沒有找到這個函數,則返回不作任何改動的對象
   if(position){
     return this;
   }
   //若是數組的第一個函數纔是所須要刪除的對應listener函數,則直接移除
   if(position===0){
     list.shift();
   }else{
     list.splice(position,1);
   }
   if(list.length===1)
     events[type]=list[0];
   if(events.removeListener!==undefined)
     this.emit('removeListener',type,listener);
   }
   return this;
}
  • 若是在之間設置了對於移除這個特殊事件「removeListener」的監聽,那麼就會在移除事件時候觸發「removeListener」事件。

最後來看removeAllListener,這個與removeListener類似,只要找到傳入的type所對應屬性的值,沒有遍歷過程,直接刪除這個屬性便可。

除此以外,還有其餘的相似與once、setMaxListeners、listeners也能夠在此基礎上實現,就不一一舉例。

5、總結

本文從node的EventEmitter模塊的使用出發,介紹了EventEmitter提供的經常使用API,而後介紹了node中基於EventEmitter的異常處理,最後本身實現了一個較爲完整的EventEmitter模塊。

相關文章
相關標籤/搜索