Node定时器和process.nextTick()

237 阅读1分钟

在开发中,我们有时候会需要“延迟”的去执行某项任务。Node提供了一些非IO异步API,可以让我们达到所需的效果。

定时器

setTimeout()

在设定的时间到达之后执行回调函数。例如,在100ms之后执行回调函数:

setTimeout(() => console.log('callback to be called.'), 100);

setInterval()

定时重复执行回调函数。例如,每隔大约100ms执行一次回调函数:

setInterval(() => console.log('callback to be called.'), 100);

setImmediate()

在IO事件回调之后立即执行的回调函数。例如:

const fs = require('fs');

fs.readFile('./package.json', (err, data) => {
  // 为了表明setImmediate回调是在IO回调之后执行,并无实际意义
  setTimeout(() => console.log('setTimeout callback.'), 0);

  setImmediate(() => console.log('setImmediate callback.'));
});

// 输出结果:
// setImmediate callback.
// setTimeout callback.

需要注意的是,定时器的定时并非精确的,它的实际执行时间受到系统调用和其他回调函数执行的影响。下面的例子中,定时器回调函数的执行就比预期的时间要晚,因为它受到读取文件IO事件回调函数执行时长的影响:

const fs = require('fs');

const startTime = Date.now();

setTimeout(() => {
  const delay = Date.now() - startTime;

  console.log(`${delay} ms have passed`);
}, 100);

fs.readFile('./package.json', (err, data) => {
  while(Date.now() - startTime < 150);
});

// 输出结果:
// 150 ms have passed

process.nextTick()

添加回调到微任务队列(next tick queue)中。通过该方法添加的所有回调函数会在事件循环继续之前全部执行完成。因此,如果使用不当(递归调用),会”饿死“事件循环。更详细的资料,可以参考官方文档

为了更直观的理解 process.nextTick 执行时机,我们来看看下面的例子:

setTimeout(() => {
  console.log('setTimeout callback.');

  process.nextTick(() => console.log('nextTick2.'));
}, 0);

setImmediate(() => {
  console.log('setImmediate callback');

  process.nextTick(() => console.log('nextTick3.'));
  process.nextTick(() => console.log('nextTick4.'));
});

process.nextTick(() => console.log('nextTick1.'));

console.log('start');

// 输出结果:
// start
// nextTick1.
// setTimeout callback.
// nextTick2.
// setImmediate callback
// nextTick3.
// nextTick4.

由上面的执行结果可知,在事件循环的每个阶段中通过process.nextTick添加的回调函数,会在“本阶段结束下阶段开始前”全部执行完毕。

另外,在官方文档的描述中,在IO事件回调之外调用的setTimeout(() => {}, 0)setImmediate(()=>{})的执行顺序是随机的。只有在IO事件回调里面,setImmediate一定先于setTimeout执行,如上文中的例子。这是因为事件循环中poll阶段的下一个阶段是check阶段。

补充

Node@v11.0.0中,提供了process.nextTick的替代API queueMicrotask

process.nextTick接受额外的参数作为执行回调函数时传递给函数的参数,而queueMicrotask则需要用闭包或者函数绑定(bind)的方式实现。例如:

process.nextTick((a, b) => {
  console.log(`nextTick: ${a} + ${b} = ${a + b}.`);
}, 1, 2);

queueMicrotask(((a, b) => {
  console.log(`queueMicrotask: ${a} + ${b} = ${a + b}.`);
}).bind(null, 1, 2));

另外,process.nextTick执行优先级高于queueMicrotask。如下所示:

queueMicrotask(() => console.log('queueMicrotask.'));

process.nextTick(() => console.log('nextTick.'));

Promise.resolve('resolved').then(console.log);

console.log('start');

// 输出结果:
// start
// nextTick.
// queueMicrotask.
// resolved