1. 非I/O的异步API
- setTimeout()
- setInterval()
- process.nextTick()
//由于事件循环自身特点,定时器的精度不够。
//采用定时器需要动用红黑树,创建定时器对象和迭代等操作,setTimemout(fn, 0)的方式较为浪费性能。
//process.nextTick()方法的操作相对较为轻量
process.nextTick = function(callback){
if(process._exting) return;
if(tickDepth >= process.maxTickDepth) maxTickWarn();
var tock = { callback: callback};
if(process.domain) tock.domain = process.domain;
nextTickQueue.push(tock);
if(nextTickQueue.length){
process._needTickCallback();
}
}
//定时器中采用红黑树的操作时间复杂度为0(lg(n)),nextTick()为0(1)
- setImmediate()
process.nextTick(function(){
console.log('nextTick执行')
})
setImmediate(function(){
console.log('setImmediate执行')
})
//执行结果如下
nextTick延迟执行
setImmediate执行
可以看出,process.nextTick()回调函数执行的优先级更高,这是因为事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idel观察者,setImmediate()属于check观察者。在轮询检查中:
idel观察者 > I/O观察者 > check观察者
在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。
在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。
(function() {
//加入两个nextTick()的回调函数
process.nextTick(function() {
console.log('nextTick延迟执行 ---- 1 ');
});
process.nextTick(function() {
console.log('nextTick延迟执行 ---- 2 ');
});
//加入两个setImmediate()的回调函数
setImmediate(function() {
console.log('setImmediate延迟执行 ---- 1');
//进入下次循环
process.nextTick(function() {
console.log('插入一次nextTick执行');
})
})
setImmediate(function() {
console.log('setImmediate延迟执行 ---- 2');
})
console.log('正常执行');
})()
//打印结果如下
正常执行
nextTick延迟执行 ---- 1
nextTick延迟执行 ---- 2
setImmediate延迟执行 ---- 1
setImmediate延迟执行 ---- 2
插入一次nextTick执行
2. 事件发布/订阅模式
//订阅
emitter.on('event1', callback(msg));
//发布
emitter.emit('event1', 'I am node.js!');
可以看到,订阅事件就是一个高阶函数的应用。事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又称为事件侦听器。
从另一种角度来看,事件侦听器也是一种钩子(hook)机制,利用钩子导出内部数据或者状态给外部的调用者。Node中的很多对象大多具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,我们就无法获取对象在运行期间的中间值或内部状态。这种通过事件钩子的方式,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。例如下面场景:
var options = {
hots: 'm.ximalaya.com',
port: 80,
path: '/login',
method: 'POST'
};
var req = http.requrest(options, function(res){
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function(chunk){
console.log('BODY: ' + chunk);
});
res.on('end', function(){
//todo
});
});
req.on('error', function(err){
console.log('problem with request: ' + err.message);
});
req.write('data\n');
req.write('data\n');
req.end();
两个对事件发布/订阅的机制做了一些额外的处理,这大多是基于扩展性而考虑的。
- 如果对一个事件添加了超过10个侦听器,将会得到一条警告。这一处设计与Node自身单线程运行有关,设计者认为侦听器太多可能导致内存泄漏,所以存在这一条警告。调用
emitter.setMaxListeners(0)
,可以将这个限制去掉。 - 为了处理异常,
EventEmitter
对象对error
事件进行了特殊对待。如果运行期间的错误触发了error事件,EventEmitter
会检查是否有对error
事件添加过侦听器。如果添加了,这个错误将会交由该侦听器处理,否则这个错误将作为异常抛出。如果外部没有捕获器,将会中断线程。
继承events模块
如下代码是Node中Stream对象继承EventEmitter的例子:
var events = require('events');
function Stream(){
events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);
Node在util模块中封装了继承的方法。而在Node提供的核心模块中,有近半数偶读继承自EventEmitter。
利用事件队列解决雪崩问题
计算机中,缓存由于存放在内存中,访问速度十分快,常常用于加速数据访问,让绝大多数的请求不必重复去做一些低效的数据读取。所谓雪崩问题,就是在高访问量、大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体的响应速度。
以下是一条数据库查询语句的调用:
var select = function(callback){
db.select('SQL', function(res){
callback(res)
})
}
如果站点刚好启动,这时缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,会影响服务的整体性能。一种改进方案是添加一个状态锁,相关代码如下:
var status = 'ready';
var select = function(callback){
if(status === 'ready'){
status = 'pending';
db.select('SQL', function(res){
status = 'ready';
callback(res)
})
}
}
但是在这种场景下,连续多次调用select()
时,只有第一次调用是生效的,后续的select()
是没有数据服务的,这时可以引入事件队列:
var proxy = new events.EventEmitter();
var status = 'ready';
var select = function(callback){
proxy.once('selected', callback);
if(status === 'ready'){
status = 'pending';
db.select('SQL', function(res){
proxy.emit('selected', res);
status = 'ready';
})
}
}