首页 / Node.js 教程 / nextTick、setImmediate 与定时器

Node.js 教程

nextTick、setImmediate 与定时器

本教程共 76 篇 · 第 15 篇 · 更新于 2026-07-25 · 约 5 分钟阅读

Node.jsnextTicksetImmediate定时器事件循环

15. nextTick、setImmediate 与定时器

本节目标:process.nextTick、setImmediate、setTimeout 的执行时机与区别。

上一章把事件循环的阶段串了一遍。这一章聚焦三个“看起来都像延后执行”的 API:

  • setTimeout(cb, delay) / setInterval(cb, delay)
  • setImmediate(cb)
  • process.nextTick(cb)

它们名字相似,行为却天差地别。用错地方,轻则时序混乱,重则 I/O 饿死。

setTimeout 与 setInterval

setTimeout 是最基础的定时器:

setTimeout(() => {
  console.log('两秒后执行');
}, 2000);

第二个参数是最小延迟毫秒数,不是精确时间。事件循环如果正忙着处理其他回调,你的定时器会被顺延。delay 设为 0 不代表立刻执行,只是把回调推进 timers 队列,等同步代码和微任务走完才会轮到:

setTimeout(() => console.log('timeout 0'), 0);
console.log('sync');

// 输出:
// sync
// timeout 0

setInterval 同理,只不过它会反复触发,直到你调用 clearInterval

const id = setInterval(() => console.log('tick'), 1000);

// 十秒后停掉
setTimeout(() => clearInterval(id), 10000);
Warning

setInterval 不关心上一次回调是否执行完。如果回调本身耗时超过间隔,回调会堆叠执行。需要“等上一个做完再安排下一个”,用递归 setTimeout

function tick() {
  console.log('doing work...');
  setTimeout(tick, 1000);
}
tick();

在 Node.js 里,setTimeoutsetInterval 返回的是一个 Timeout 对象,不是浏览器里的数字 ID。不过你依然可以把它传给 clearTimeout / clearInterval

const timer = setTimeout(() => {}, 1000);
clearTimeout(timer);

setImmediate:poll 之后的“立即”

setImmediate 是 Node.js 独有的 API,浏览器里没有(除旧版 IE/Edge)。它把回调安排在当前事件循环的 check 阶段执行:

setImmediate(() => {
  console.log('immediate');
});

如果把 setImmediatesetTimeout(..., 0) 放在主模块里,顺序是不确定的。因为 timers 阶段的执行时机取决于进程启动速度和系统调度:

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

// 可能输出 timeout -> immediate,也可能反过来

但只要把它们放进 I/O 回调,顺序就固定了:

import { readFile } from 'node:fs';

readFile('any.txt', () => {
  setTimeout(() => console.log('timeout'), 0);
  setImmediate(() => console.log('immediate'));
});

// 一定输出 immediate -> timeout

原因:I/O 回调在 poll 阶段执行,poll 之后紧接着 check 阶段,所以 setImmediate 先跑。setTimeout 要等到下一轮 timers 阶段。

process.nextTick:最急的插队者

process.nextTick 把回调塞进 nextTickQueue,在当前操作完成后、事件循环进入下一阶段之前执行。它比其他任何队列都优先:

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// 输出顺序:
// nextTick
// promise
// timeout
// immediate

nextTick 的强大伴随着危险。如果你递归调用它:

function dangerous() {
  process.nextTick(dangerous);
}
dangerous();

事件循环永远到不了 poll 阶段,所有 I/O 回调、timers、setImmediate 全部饿死。你的进程 CPU 占用 100%,却什么正事都干不了。

Warning

不要把 process.nextTick 当成普通调度器滥用。Node.js 官方推荐:能不用就不用,优先使用 setImmediate,因为它的语义更容易推理。

三者的执行顺序速查

在一个普通的 I/O 回调里:

import { readFile } from 'node:fs';

readFile(import.meta.filename, () => {
  process.nextTick(() => console.log('A: nextTick'));
  Promise.resolve().then(() => console.log('B: promise'));
  setImmediate(() => console.log('C: immediate'));
  setTimeout(() => console.log('D: timeout'), 0);
});

输出:

A: nextTick
B: promise
C: immediate
D: timeout

记忆口诀:同操作完 → nextTick → Promise → 宏任务(immediate / timeout)

什么时候用哪个

API使用场景
setTimeout(fn, n)真正的延迟执行,最小延迟 n 毫秒。
setInterval(fn, n)周期性任务。注意回调堆积问题,必要时换递归 setTimeout
setImmediate(fn)希望当前 I/O 批处理完后尽快执行。比 setTimeout(fn, 0) 语义更干净。
process.nextTick(fn)必须在本轮操作结束后、任何 I/O 之前执行。例如保证 API 的异步一致性、在构造函数里延迟触发事件。

一个实用的 nextTick 场景:让同步 API 表现出异步接口。

function api(arg, callback) {
  if (typeof arg !== 'string') {
    // 即使校验失败,也异步抛错,保持接口一致性
    process.nextTick(callback, new TypeError('arg must be string'));
    return;
  }
  // ... 真正异步操作
}

如果直接 callback(new TypeError(...)),调用者会觉得这是同步出错。用 nextTick 推迟一下,就统一了异步语义。

定时器的精度问题

JavaScript 定时器不是实时系统。delay 只是最小等待时间,实际执行受事件循环负载影响。做动画或高频采样,别依赖 setInterval 的精度。Node.js 里有更专业的方案,比如 process.hrtime.bigint() 做高精度计时。

另外,如果 delay 超过 2147483647 毫秒(约 24.8 天),Node.js 会把它当 1 处理。超长延迟需求,得自己包一层逻辑。