1. 非I/O的异步API

  1. setTimeout()
  2. setInterval()
  3. process.nextTick()
  1. //由于事件循环自身特点,定时器的精度不够。
  2. //采用定时器需要动用红黑树,创建定时器对象和迭代等操作,setTimemout(fn, 0)的方式较为浪费性能。
  3. //process.nextTick()方法的操作相对较为轻量
  4. process.nextTick = function(callback){
  5. if(process._exting) return;
  6. if(tickDepth >= process.maxTickDepth) maxTickWarn();
  7. var tock = { callback: callback};
  8. if(process.domain) tock.domain = process.domain;
  9. nextTickQueue.push(tock);
  10. if(nextTickQueue.length){
  11. process._needTickCallback();
  12. }
  13. }
  14. //定时器中采用红黑树的操作时间复杂度为0(lg(n)),nextTick()为0(1)
  1. setImmediate()
  1. process.nextTick(function(){
  2. console.log('nextTick执行')
  3. })
  4. setImmediate(function(){
  5. console.log('setImmediate执行')
  6. })
  7. //执行结果如下
  8. nextTick延迟执行
  9. setImmediate执行

可以看出,process.nextTick()回调函数执行的优先级更高,这是因为事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idel观察者,setImmediate()属于check观察者。在轮询检查中:

idel观察者 > I/O观察者 > check观察者

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。
在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。

  1. (function() {
  2. //加入两个nextTick()的回调函数
  3. process.nextTick(function() {
  4. console.log('nextTick延迟执行 ---- 1 ');
  5. });
  6. process.nextTick(function() {
  7. console.log('nextTick延迟执行 ---- 2 ');
  8. });
  9. //加入两个setImmediate()的回调函数
  10. setImmediate(function() {
  11. console.log('setImmediate延迟执行 ---- 1');
  12. //进入下次循环
  13. process.nextTick(function() {
  14. console.log('插入一次nextTick执行');
  15. })
  16. })
  17. setImmediate(function() {
  18. console.log('setImmediate延迟执行 ---- 2');
  19. })
  20. console.log('正常执行');
  21. })()
  22. //打印结果如下
  23. 正常执行
  24. nextTick延迟执行 ---- 1
  25. nextTick延迟执行 ---- 2
  26. setImmediate延迟执行 ---- 1
  27. setImmediate延迟执行 ---- 2
  28. 插入一次nextTick执行

2. 事件发布/订阅模式

  1. //订阅
  2. emitter.on('event1', callback(msg));
  3. //发布
  4. emitter.emit('event1', 'I am node.js!');

可以看到,订阅事件就是一个高阶函数的应用。事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又称为事件侦听器。
从另一种角度来看,事件侦听器也是一种钩子(hook)机制,利用钩子导出内部数据或者状态给外部的调用者。Node中的很多对象大多具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,我们就无法获取对象在运行期间的中间值或内部状态。这种通过事件钩子的方式,可以使编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。例如下面场景:

  1. var options = {
  2. hots: 'm.ximalaya.com',
  3. port: 80,
  4. path: '/login',
  5. method: 'POST'
  6. };
  7. var req = http.requrest(options, function(res){
  8. console.log('STATUS: ' + res.statusCode);
  9. console.log('HEADERS: ' + JSON.stringify(res.headers));
  10. res.setEncoding('utf8');
  11. res.on('data', function(chunk){
  12. console.log('BODY: ' + chunk);
  13. });
  14. res.on('end', function(){
  15. //todo
  16. });
  17. });
  18. req.on('error', function(err){
  19. console.log('problem with request: ' + err.message);
  20. });
  21. req.write('data\n');
  22. req.write('data\n');
  23. req.end();

两个对事件发布/订阅的机制做了一些额外的处理,这大多是基于扩展性而考虑的。

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

继承events模块

如下代码是Node中Stream对象继承EventEmitter的例子:

  1. var events = require('events');
  2. function Stream(){
  3. events.EventEmitter.call(this);
  4. }
  5. util.inherits(Stream, events.EventEmitter);

Node在util模块中封装了继承的方法。而在Node提供的核心模块中,有近半数偶读继承自EventEmitter。

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

计算机中,缓存由于存放在内存中,访问速度十分快,常常用于加速数据访问,让绝大多数的请求不必重复去做一些低效的数据读取。所谓雪崩问题,就是在高访问量、大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大的查询请求,进而往前影响到网站整体的响应速度。
以下是一条数据库查询语句的调用:

  1. var select = function(callback){
  2. db.select('SQL', function(res){
  3. callback(res)
  4. })
  5. }

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

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

但是在这种场景下,连续多次调用select()时,只有第一次调用是生效的,后续的select()是没有数据服务的,这时可以引入事件队列:

  1. var proxy = new events.EventEmitter();
  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(res){
  8. proxy.emit('selected', res);
  9. status = 'ready';
  10. })
  11. }
  12. }