setTimeout,setInterval,process.nextTick,setImmediate in Node.js

尽管我们在介绍Node的时候,多数情况下都会提到异步I/o,但是Node中其实还存在一些与I/o无关的异步API,它们分别是setTimeout(),setInterval(),setImmediate()和process.nextTick()。

##1 setTimeout()和setInterval()
setTimeout()和setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。他们的实现原理与异步I/o比较类似,只是不需要I/o线程池的参与。

调用setTimeout()或setInterval()时创建的计时器会被放入定时器观察者内部的红黑树中,每次Tick时,会从该红黑树中检查定时器是否超过定时时间,超过的话,就立即执行对应的回调函数。setTimeout()和setInterval()都是当定时器使用,他们的区别在于后者是重复触发,而且由于时间设的过短会造成前一次触发后的处理刚完成后一次就紧接着触发。

由于定时器是超时触发,这会导致触发精确度降低,比如用setTimeout设定的超时时间是5秒,当事件循环在第4秒循到了一个任务,它的执行时间3秒的话,那么setTimeout的回调函数就会过期2秒执行,这就是造成精度降低的原因。并且由于采用红黑树和迭代的方式保存定时器和判断触发,较为浪费性能。

##2 process.nextTick()
在未了解process.nextTick()之前,很多人也许为了立即异步执行一个任务,会这样调用setTimeout()来达到所需的效果:

setTimeout(function() {
  //TODO
}, 0);

由于事件循环自身的特点,定时器的精确度不够。而事实上,采用定时器需要采用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn, 0)的方式较为浪费性能。实际上,process.nextTick()方法的操作相对较为轻量,每次调用Process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器采用红黑树的操作时间复杂度为o(lg(n)),而nextTick()的时间复杂度为o(1)。相较之下,process.nextTick()更高效。

##3 setImmediate()
setImmediate()方法与process.nextTick()方法十分类似,都是将回调函数延迟执行。在Node v0.9.1之前,setImmediate()还没有实现,那时候实现类似的功能主要是通过process.nextTick()来完成,该方法的代码如下所示:

process.nextTick(function() {
  console.log('延迟执行');
});
console.log('正常执行');

上述代码的输出结果如下:
正常执行
延迟执行
而用setImmediate()实现时,相关代码如下:

setImmediate(function() {
console.log('延迟执行');
});
console.log('正常执行');

结果完全一样:
正常执行
延迟执行

但是两者之间其实有细微的差别。将他们放在一起时,又会是怎样的优先级呢,示例代码如下:

process.nextTick(function() {
  console.log('nextTick延迟执行');
});
setImmediate(function() {
  console.log('setImmediate延迟执行');
});
console.log('正常执行');

执行结果如下:
正常执行
nextTick延迟执行
setImmediate延迟执行

从结果可以看到,process.nextTick()的优先级要高于setImmediate()。这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者,在每一个轮循环检查中,idle观察者先于I/o观察者,I/o观察者先于check观察者。
在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是堡村子啊链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。示例代码如下:

//加入两个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('强势插入');
  });
});
setImmediate(function() {
  console.log('setImmediate延迟执行2');
});
console.log('正常执行');

执行结果如下:
正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
强势插入
setImmediate延迟执行2

从执行结果上可以看出,当第一个setImmediate()的回调函数执行后,并没有立即执行第二个,而是进入了下一轮循环,每次按process.nextTick()优先,setImmeiate()次后的顺序执行。之所以这样设计,是为了保证每轮循环能够较快的执行结束,防止CPU占用过多而阻塞后续I/o调用的情况。