nextTick、setImmediate 与定时器
本教程共 76 篇 · 第 15 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
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 里,setTimeout 和 setInterval 返回的是一个 Timeout 对象,不是浏览器里的数字 ID。不过你依然可以把它传给 clearTimeout / clearInterval:
const timer = setTimeout(() => {}, 1000);
clearTimeout(timer);
setImmediate:poll 之后的“立即”
setImmediate 是 Node.js 独有的 API,浏览器里没有(除旧版 IE/Edge)。它把回调安排在当前事件循环的 check 阶段执行:
setImmediate(() => {
console.log('immediate');
});
如果把 setImmediate 和 setTimeout(..., 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 处理。超长延迟需求,得自己包一层逻辑。