Node.js 定时器与调度:setTimeout、setImmediate 与队列心智
事件循环那篇讲过阶段的大致轮廓。写业务时更常遇到的是:定时任务放哪、顺序为什么和我想的不一样、
unref是干嘛的。
这一篇把 定时器 API 摊开:setTimeout / setInterval、setImmediate、process.nextTick 在调度上的差别,以及 在服务端长进程里 怎么少给自己挖坑。
setTimeout / setInterval:延迟与周期
setTimeout(fn, ms):至少 ms 毫秒后把回调放进 定时器阶段 的队列(「至少」意味着忙时会更晚)。
setInterval:按间隔重复调度;要注意:
- 若回调执行时间 长于间隔,会出现 堆积 或 表现成串行(视实现与是否等待上次完成而定);
- 长周期任务更常见写法是:async 函数尾部再
setTimeout自己,而不是盲目setInterval。
服务端习惯:
- 短延迟(debounce、重试退避)用
setTimeout; - 周期巡检 要评估 与事件循环抢时间 的成本,必要时 拆到独立 Worker/进程。
setImmediate:本轮 I/O 后尽快跑
setImmediate(fn) 把回调放在 check 阶段(相对定时器与 I/O 的位置以 Node 文档为准)。
和 setTimeout(fn, 0) 的对比:
- 在 I/O 回调内部,
setImmediate往往 先于setTimeout(0)执行——具体顺序属于 易错题,线上逻辑 不要依赖「谁先谁后」的边角顺序,除非写测试锁死版本。
工程建议:
- 跨宏任务顺序敏感 的代码,改成 显式 Promise 链 或 单一队列,比赌
setImmediate更稳。
process.nextTick:高优先级「插队」
nextTick 的回调在 当前阶段结束后、进入下一阶段前 执行,队列 清空完才继续。
后果:
- 滥用 会让 I/O 与定时器饿死(starvation);
- 递归
nextTick可以 卡死事件循环。
适用:
- 极少量、需要 在同步代码返回后立刻 跑的逻辑;
- 库作者维护 内部不变量 时用得多,业务层尽量 少用。
ref 与 unref:别让定时器拖住进程退出
默认定时器 持有事件循环引用,进程 还有挂起的 timer 就不会退出。
timeout.unref()(以及部分句柄的 unref)表示:
- 还在跑服务时 timer 照常触发;
- 没有其它活计时,进程 可以正常退出。
典型场景:
- 心跳、清理任务 在 CLI 或测试里不想 挡住
process.exit。
反过来 ref() 恢复默认行为。
与 Promise 微任务:顺序别混着赌
一轮里大致有:
- 同步代码;
- 微任务(Promise
then); - 宏任务 / 各阶段(timer、I/O、immediate、close…)。
业务代码 优先用 async/await + Promise 表达流程;定时器 用于 时间维度 的调度。需要 精确顺序 时写 小测试 钉死 Node 大版本,而不是凭记忆背表。
小结:定时器是「时间 + 事件循环」的合约
setTimeout/setInterval:按时间驱动,注意 堆积与漂移;setImmediate:尽快在 I/O 后跑,避免依赖与 timeout(0) 的细微竞态;nextTick:少用在业务,防饿死;unref:控制 进程是否被 timer 吊住。
把这几条和 事件循环、优雅退出(下一篇)连起来,长驻服务的生命周期会清晰很多。