Gong Yong的Blog

nodejs中的events模块和EventEmitter对象

事件是nodejs中的一个核心概念,node所声称的非阻塞I/O就是通过事件驱动的模式实现的,而node使用js语言来编写程序,js本身就具有事件驱动的特性。在node中,基本上很多工作都是通过事件驱动完成的,处理用户请求、读写文件、从数据库获取数据等都是通过注册和触发事件完成的。

我们先看看node官网上提供的那个经典的创建http服务器处理客户请求的示例:

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

这段代码先加载http核心模块然后调用httpcreateServer方法创建一个http服务器,这个方法的参数就是用于处理客户端请求的函数,这段代码实际上监听了request事件,而所传入的函数就是事件处理函数,当有请求到达的时候就会触发这个事件,进而调用这个处理函数。这段代码还可以这么写。

var http = require('http');
var server = http.createServer();
server.on('request',function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
});
server.listen(1337,'127.0.0.1');

这样一眼就可以看出我们监听了request这个事件。这个事件是http这个核心模块所提供的事件,node的核心模块还提供了很多类似的事件,但这些都是已经提供好的,我们怎么使用node的事件机制来编写自己的事件呢?

先看一个普通的示例:

var events = require('events');
var eventEmitter = new events.EventEmitter();
 
var ringBell = function ringBell()
{
  console.log('ring ring ring');
}
eventEmitter.on('doorOpen', ringBell);
 
eventEmitter.emit('doorOpen');

运行这段代码会在控制台输出ring ring ring,这段代码先加载了node的events模块,这也是node提供的核心模块,然后创建了一个EventEmitter的实例,这个类就是用于处理事件注册和监听工作的。在这个示例里我们调用EventEmitter对象的on方法,将ringBell这个函数作为事件处理函数注册到doorOpen这个事件上,然后再调用EventEmitter对象的emit方法触发这个事件,这样ringBell方法就会被调用。

整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。一个事件上可以注册多个处理函数,就以doorOpen这个事件为例,我们可以使用EventEmitter.on注册多个处理函数:

eventEmitter.on('doorOpen', ringBell);
eventEmitter.on('doorOpen', doSomething);
eventEmitter.on('doorOpen', doSomethingElse);

eventEmitter.emit('doorOpen');

最后当调用EventEmitter.emit触发这个事件后所有的事件处理函数都会执行。node默认一个事件最多可以注册10个处理函数,这个值可以修改。我们也可以给事件处理函数传递参数,假设我们把门打开后的声音可能会不断变化,并且这个声音要在开门的时候传递:

eventEmitter.on(‘doorOpen’, function(ring)
{
    Console.log(ring);
});
eventEmitter.emit(‘doorOpen’, ‘ringeling’);

doorOpen这个事件的处理函数接收一个参数,这个参数就是在emit方法中传入的,所有需要传入事件处理函数的参数都在emit的第一个参数后面传入。

通过上面的示例我们基本上了解了node中的事件模型是怎么使用的,但这个时候会有一个疑问,我们可以看到上面http服务器的示例中直接调用createServer这个方法就注册了request这个事件,所以我们的门能不能提供一个方法注册铃声响起这个事件,我们将这个事件命名为ringing,并且有一个open的方法,在门被打开的时候调用这个方法,然后触发ringing这个事件呢?

答案是可以:

//door.js
var events = require('events');
var util = require('util');

function Door(colour) {
  this.colour = colour;
  events.EventEmitter.call(this);

this.open = function(sound){
     this.emit('ringing,sound);
  };

this.addRing = function(handleRinging){
     this.on('ringing,handleRinging);
  };
}

util.inherits(Door, events.EventEmitter);

module.exports = Door;

//use-door.js
var Door = require('./door');
var frontDoor = new Door('brown');
frontDoor.addRing(function(sound){
    console.log(sound);
});

frontDoor.open('ringing...');

我们首先创建了一个door的模块,这个模块会提供一个Door的类,这个类继承了events.EventEmiiter

function Door(colour) {
  this.colour = colour;
  events.EventEmitter.call(this);
}

util.inherits(Door, events.EventEmitter);

Door构造函数中的events.EventEmitter.call(this)是调用EventEmitter的构造方法,util.inherits(Door, events.EventEmitter)EventEmiiter的属性和方法拷贝到Door对象中,这样我们就可以直接在Door方法中调用this.onthis.emit这两个方法了。接下来我们Door提供了两个方法,addRing用于给门安装铃铛,open方法用于开门。接下来我们在use-door.js中创建一个Door对象,然后调用addRing安装门铃,调用这个方法需要传入一个函数,这个函数就是用于处理门铃事件的,然后调用open方法开门。这样我就实现了一个门的类,这个类中有一个ringing的事件,这个事件会在安装门铃的时候注册,并且会在开门的时候触发。

关于EventEmiiter中的一些跟事件相关的方法和属性请参加具体文档,关于使用node中的events模块和EventEmitter这个对象的内容就介绍到这里,希望对你有帮助,改天有时间我们再介绍一下EventEmitter的其他一些特性。