大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
定时器与 nextTick
nodejs-essentials 的 node-async 介绍了 Event Loop 概念。本课深入 定时器 API 与调度优先级:setTimeout、setInterval、setImmediate、process.nextTick——理解它们何时执行、为何顺序有时「反直觉」。
学习目标
- 区分宏任务(timers、I/O、check)与微任务(nextTick、Promise)
- 正确使用
setImmediate与setTimeout(fn, 0)的场景差异 - 避免
nextTick递归导致 Event Loop 饥饿
四类调度 API
| API | 阶段 | 典型用途 |
|---|---|---|
process.nextTick(fn) | 当前阶段结束后、下一轮 Loop 前 | 保证回调在 I/O 之前执行 |
Promise.then / queueMicrotask | 微任务队列 | 异步链、与 nextTick 交错 |
setTimeout / setInterval | timers 阶段 | 延迟、周期任务 |
setImmediate | check 阶段 | I/O 回调之后尽快执行 |
Node 文档:nextTick 优先级高于 Promise 微任务(与浏览器略有不同)。
执行顺序实验
console.log("1 sync");
setTimeout(() => console.log("2 setTimeout"), 0);
setImmediate(() => console.log("3 setImmediate"));
process.nextTick(() => console.log("4 nextTick"));
Promise.resolve().then(() => console.log("5 promise"));
console.log("6 sync");
主脚本里常见输出:1 → 6 → 4 → 5 → 2/3(2 与 3 在 I/O 上下文外顺序不保证)。在 fs.readFile 回调内 再设 timeout 与 immediate,则 immediate 通常先于 timeout——因为 check 阶段紧跟 poll。
setTimeout 的坑
- 最小延迟:浏览器与 Node 对
0ms都有下限(通常 1ms 量级),不能当作「立即」。 - drift:
setInterval不补偿执行耗时,长任务会导致间隔「挤在一起」;精确周期用setTimeout链或timers/promises的scheduler.wait(Node 实验 API)。 - unref:
timer.unref()让定时器不阻止进程退出——CLI 一次性脚本常用。
const t = setTimeout(() => {}, 60_000);
t.unref(); // 无其他 handle 时进程可正常结束
nextTick 适用与滥用
适用:在事件回调里抛错前做同步清理;保证 emit('error') 前 listener 已注册(见 nva-events)。
滥用:递归 process.nextTick 会 饿死 I/O 与 timers——相当于占满微任务队列。
// 反模式:阻塞 Event Loop
function spin() {
process.nextTick(spin);
}
CPU 密集应改用 nrt-worker 或拆分到 nrt-child。
与 Spring @Scheduled 对照
| Node timers | Spring | |
|---|---|---|
| 线程 | 单线程回调 | 线程池调度 |
| 精度 | 毫秒级,受 Loop 负载影响 | cron / fixedRate 更语义化 |
| 取消 | clearTimeout / clearInterval | @Scheduled 需容器管理 |
练习
- 运行上文顺序实验,在 readFile 回调内再测 setImmediate vs setTimeout
- 写
debounce(fn, ms),内部用setTimeout+clearTimeout - 对一次性 CLI 脚本尝试
timer.unref(),观察进程是否提前退出
小结
nextTick 最快但最危险;setImmediate 贴近 I/O 后;setTimeout 看 timers 阶段。 下一课回调与错误栈——异步里 stack trace 为何「断档」。