循序渐进教你实现一个完整的node的EventEmitter模块

1,433 阅读13分钟
原文链接: github.com

循序渐进教你实现一个完整的node的EventEmitter模块


node的事件模块只包含了一个类:EventEmitter。这个类在node的内置模块和第三方模块中大量使用。EventEmitter本质上是一个观察者模式的实现,这种模式可以扩展node在多个进程或网络中运行。本文从node的EventEmitter的使用出发,循序渐进的实现一个完整的EventEmitter模块。

  • EventEmitter模块的基本用法和简单实现
  • node中常用的EventEmitter模块的API
  • EventEmitter模块的异常处理
  • 完整的实现一个EventEmitter模块

一、EventEmitter模块的基本用法和简单实现

(1) EventEmitter模块的基本用法

首先先了解一下EventEmitter模块的基本用法,EventEmitter本质上是一个观察者模式的实现,所谓观察者模式:

它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

因此最基本的EventEmitter功能,包含了一个观察者和一个被监听的对象,对应的实现就是EventEmitter中的on和emit:

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方法监听,从而执行相应的函数。

(2) 简单实现一个EventEmitter模块

根据上述的例子,我们知道了EventEmitter模块的基础功能emit和on。下面我们实现一个包含emit和on方法的EventEmitter类。

on(eventName,callback)方法传入两个参数,一个是事件名(eventName),另一个是相应的回调函数,我们选择在on的时候针对事件名添加监听函数,用对象来包含所有事件。在这个对象中对象名表示事件名(eventName),而对象的值是一个数组,表示该事件名所对应的执行函数。

emit(eventName,...arg)方法传入的参数,第一个为事件名,其他参数事件对应的执行函数中的实参,emit方法的功能就是从事件对象中,寻找对应key为eventName的属性,执行该属性所对应的数组里面每一个执行函数。

下面来实现一个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

二、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"事件。

三、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)})

这样就能在最外层捕获异步或者说非阻塞函数中的异常。

四、完整的实现一个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也可以在此基础上实现,就不一一举例。

五、总结

本文从node的EventEmitter模块的使用出发,介绍了EventEmitter提供的常用API,然后介绍了node中基于EventEmitter的异常处理,最后自己实现了一个较为完整的EventEmitter模块。