首页 > 代码库 > Node.js入门:事件机制

Node.js入门:事件机制

Evented I/O for V8 JavaScript

    基于V8引擎实现的事件驱动IO。
 

事件机制的实现

    Node.js中大部分的模块,都继承自Event模块(http://nodejs.org/docs/latest/api/events.html )。Event模块(events.EventEmitter)是一个简单的事件监听器模式的实现。具有addListener/on,once,removeListener,removeAllListeners,emit等基本的事件监听模式的方法实现。它与前端DOM树上的事件并不相同,因为它不存在冒泡,逐层捕获等属于DOM的事件行为,也没有preventDefault()、stopPropagation()、stopImmediatePropagation() 等处理事件传递的方法。
    从另一个角度来看,事件侦听器模式也是一种事件钩子(hook)的机制,利用事件钩子导出内部数据或状态给外部调用者。Node.js中的很多对象,大多具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,对象运行期间的中间值或内部状态,是我们无法获取到的。这种通过事件钩子的方式,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。
 1 var options = { 
 2     host: ‘www.google.com‘, 
 3     port: 80, 
 4     path: ‘/upload‘, method: ‘POST‘
 5 }; 
 6 var req = http.request(options, function (res) { 
 7     console.log(‘STATUS: ‘ + res.statusCode); 
 8     console.log(‘HEADERS: ‘ + JSON.stringify(res.headers)); 
 9     res.setEncoding(‘utf8‘); 
10     res.on(‘data‘, function (chunk) { 
11         console.log(‘BODY: ‘ + chunk); 
12     }); 
13 }); 
14 req.on(‘error‘, function (e) { 
15     console.log(‘problem with request: ‘ + e.message); 
16 }); 
17 // write data to request body 
18 req.write(‘data\n‘); 
19 req.write(‘data\n‘); 
20 req.end();

    在这段HTTP request的代码中,程序员只需要将视线放在error,data这些业务事件点即可,至于内部的流程如何,无需过于关注。

    值得一提的是如果对一个事件添加了超过10个侦听器,将会得到一条警告,这一处设计与Node.js自身单线程运行有关,设计者认为侦听器太多,可能导致内存泄漏,所以存在这样一个警告。可以将这个限制去掉,调用:
emitter.setMaxListeners(0);
    其次,为了提升Node.js的程序的健壮性,EventEmitter对象对error事件进行了特殊对待。如果运行期间的错误触发了error事件。EventEmitter会检查是否有对error事件添加过侦听器,如果添加了,这个错误将会交由该侦听器处理,否则,这个错误将会作为异常抛出。如果外部没有捕获这个异常,将会引起线程的退出。
 

继承event.EventEmitter

    实现一个继承了EventEmitter类是十分简单的,以下是Node.js中流对象继承EventEmitter的例子:
1 function Stream() { 
2     events.EventEmitter.call(this); 
3 }
4 util.inherits(Stream, events.EventEmitter);

    Node.js在工具模块中封装了继承的方法,所以此处可以很便利地调用。程序员可以通过这样的方式轻松继承EventEmitter对象,利用事件机制,可以帮助你解决一些问题。

 

多事件之间协作

    在略微大一点的应用中,数据与Web服务器之间的分离是必然的,如新浪微博、Facebook、Twitter等。这样的优势在于数据源统一,并且可以为相同数据源制定各种丰富的客户端程序。以Web应用为例,在渲染一张页面的时候,通常需要从多个数据源拉取数据,并最终渲染至客户端。Node.js在这种场景中可以很自然很方便的同时并行发起对多个数据源的请求。
1 api.getUser("username", function (profile) { 
2     // Got the profile 
3 }); 
4 api.getTimeline("username", function (timeline) { 
5     // Got the timeline 
6 }); 
7 api.getSkin("username", function (skin) { 
8     // Got the skin 
9 });

    Node.js通过异步机制使请求之间无阻塞,达到并行请求的目的,有效的调用下层资源。但是,这个场景中的问题是对于多个事件响应结果的协调并非被Node.js原生优雅地支持。为了达到三个请求都得到结果后才进行下一个步骤,程序也许会被变成以下情况:

1 api.getUser("username", function (profile) { 
2     api.getTimeline("username", function (timeline) { 
3         api.getSkin("username", function (skin) { 
4             // TODO 
5         }); 
6     }); 
7 });

    这将导致请求变为串行进行,无法最大化利用底层的API服务器。

    为解决这类问题,有一个模块(EventProxy,https://github.com/JacksonTian/eventproxy)来实现多事件协作,以下为上面代码的改进版:
 1 var proxy = new EventProxy(); 
 2 proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) { 
 3    // TODO 
 4 }); 
 5 api.getUser("username", function (profile) { 
 6     proxy.emit("profile", profile); 
 7 }); 
 8 api.getTimeline("username", function (timeline) { 
 9     proxy.emit("timeline", timeline); 
10 }); 
11 api.getSkin("username", function (skin) { 
12     proxy.emit("skin", skin); 
13 });

    EventProxy也是一个简单的事件侦听者模式的实现,由于底层实现跟Node.js的EventEmitter不同,无法合并进Node.js中。但是却提供了比EventEmitter更强大的功能,且API保持与EventEmitter一致,与Node.js的思路保持契合,并可以适用在前端中。

    这里的all方法是指侦听完profile、timeline、skin三个方法后,执行回调函数,并将侦听接收到的数据传入。
 

利用事件队列解决雪崩问题

    所谓雪崩问题,是在缓存失效的情景下,大并发高访问量同时涌入数据库中查询,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体响应缓慢。那么在Node.js中如何应付这种情景呢。
1 var select = function (callback) { 
2     db.select("SQL", function (results) { 
3         callback(results); 
4     }); 
5 };

    以上是一句数据库查询的调用,如果站点刚好启动,这时候缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,影响到服务的整体性能。一个改进是添加一个状态锁。

 1 var status = "ready"; 
 2 var select = function (callback) { 
 3     if (status === "ready") { 
 4         status = "pending"; 
 5         db.select("SQL", function (results) { 
 6             callback(results); 
 7             status = "ready"; 
 8         }); 
 9     } 
10 };

    但是这种情景,连续的多次调用select,只有第一次调用是生效的,后续的select是没有数据服务的。所以这个时候引入事件队列吧:

 1 var proxy = new EventProxy(); 
 2 var status = "ready"; 
 3 var select = function (callback) { 
 4     proxy.once("selected", callback); 
 5     if (status === "ready") { 
 6         status = "pending"; 
 7         db.select("SQL", function (results) { 
 8             proxy.emit("selected", results); 
 9             status = "ready";
10         }); 
11      } 
12 };

    这里利用了EventProxy对象的once方法,将所有请求的回调都压入事件队列中,并利用其执行一次就会将监视器移除的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的时间中永远只有一次,在这查询期间到来的调用,只需在队列中等待数据就绪即可,节省了重复的数据库调用开销。由于Node.js单线程执行的原因,此处无需担心状态问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。此处也可以用EventEmitter替代EventProxy,不过可能存在侦听器过多,引发警告,需要调用setMaxListeners(0)移除掉警告,或者设更大的警告阀值。