Node.js 定时器与调度:setTimeout、setImmediate 与队列心智

事件循环那篇讲过阶段的大致轮廓。写业务时更常遇到的是:定时任务放哪、顺序为什么和我想的不一样、unref 是干嘛的
这一篇把 定时器 API 摊开:setTimeout / setInterval、setImmediate、process.nextTick 在调度上的差别,以及 在服务端长进程里 怎么少给自己挖坑。

setTimeout(fn, ms):至少 ms 毫秒后把回调放进 定时器阶段 的队列(「至少」意味着忙时会更晚)。

setInterval:按间隔重复调度;要注意:

  • 若回调执行时间 长于间隔,会出现 堆积表现成串行(视实现与是否等待上次完成而定);
  • 长周期任务更常见写法是:async 函数尾部再 setTimeout 自己,而不是盲目 setInterval

服务端习惯:

  • 短延迟(debounce、重试退避)用 setTimeout
  • 周期巡检 要评估 与事件循环抢时间 的成本,必要时 拆到独立 Worker/进程

setImmediate(fn) 把回调放在 check 阶段(相对定时器与 I/O 的位置以 Node 文档为准)。

setTimeout(fn, 0) 的对比:

  • I/O 回调内部setImmediate 往往 先于 setTimeout(0) 执行——具体顺序属于 易错题,线上逻辑 不要依赖「谁先谁后」的边角顺序,除非写测试锁死版本。

工程建议:

  • 跨宏任务顺序敏感 的代码,改成 显式 Promise 链单一队列,比赌 setImmediate 更稳。

nextTick 的回调在 当前阶段结束后、进入下一阶段前 执行,队列 清空完才继续

后果:

  • 滥用 会让 I/O 与定时器饿死(starvation);
  • 递归 nextTick 可以 卡死事件循环

适用:

  • 极少量、需要 在同步代码返回后立刻 跑的逻辑;
  • 库作者维护 内部不变量 时用得多,业务层尽量 少用

默认定时器 持有事件循环引用,进程 还有挂起的 timer 就不会退出

timeout.unref()(以及部分句柄的 unref)表示:

  • 还在跑服务时 timer 照常触发;
  • 没有其它活计时,进程 可以正常退出

典型场景:

  • 心跳、清理任务 在 CLI 或测试里不想 挡住 process.exit

反过来 ref() 恢复默认行为。

一轮里大致有:

  • 同步代码
  • 微任务(Promise then);
  • 宏任务 / 各阶段(timer、I/O、immediate、close…)。

业务代码 优先用 async/await + Promise 表达流程;定时器 用于 时间维度 的调度。需要 精确顺序 时写 小测试 钉死 Node 大版本,而不是凭记忆背表。

  • setTimeout / setInterval:按时间驱动,注意 堆积与漂移
  • setImmediate:尽快在 I/O 后跑,避免依赖与 timeout(0) 的细微竞态
  • nextTick少用在业务,防饿死;
  • unref:控制 进程是否被 timer 吊住

把这几条和 事件循环优雅退出(下一篇)连起来,长驻服务的生命周期会清晰很多。