Node.js入门:事件机制
Evented I/O for V8 JavaScript
事件机制的实现
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这些业务事件点即可,至于内部的流程如何,无需过于关注。
emitter.setMaxListeners(0);
|
继承event.EventEmitter
1 function Stream() { 2 events.EventEmitter.call(this); 3 } 4 util.inherits(Stream, events.EventEmitter);
Node.js在工具模块中封装了继承的方法,所以此处可以很便利地调用。程序员可以通过这样的方式轻松继承EventEmitter对象,利用事件机制,可以帮助你解决一些问题。
多事件之间协作
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服务器。
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的思路保持契合,并可以适用在前端中。
利用事件队列解决雪崩问题
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)移除掉警告,或者设更大的警告阀值。