定时器回调在事件循环的timers阶段执行,属于宏任务;setTimeout和setInterval的回调被放入timers队列,需等待当前同步代码和微任务完成后,在下一轮事件循环的timers阶段按序执行,实际延迟≥设定值。

它们在事件循环的 timers 阶段 执行。
定时器回调属于宏任务,排在 timers 队列里
当你调用 setTimeout 或 setInterval,浏览器或 Node.js 会把回调函数注册进一个内部的定时器系统(比如 libuv 的最小堆)。当设定的时间到达,该回调并不会立刻运行,而是被放入 宏任务队列中的 timers 队列。
事件循环进入 timers 阶段 时,才会批量取出已到期的定时器回调,按顺序推入主线程执行。
- 这个阶段只处理
setTimeout和setInterval的回调,不处理Promise.then或queueMicrotask(那些是微任务,在当前宏任务结束后、下一个宏任务开始前执行) - 即使设了
setTimeout(fn, 0),fn 也得等当前同步代码跑完、微任务清空后,到下一轮事件循环的 timers 阶段才可能执行
实际执行时间 ≥ 设定延迟,不是“准时闹钟”
定时器的延迟值只是“最早可执行时间”的承诺,不是硬性截止时间。真正执行受三方面影响:
立即学习“Java免费学习笔记(深入)”;
- 主线程是否空闲:如果前面有长任务(如大数组排序、死循环),即使 timer 到期了,回调也只能排队等
-
后台节流:页面切到后台时,多数浏览器会把最小间隔拉高到 1000ms,
setTimeout(fn, 10)可能拖到 1s 后才触发 - 事件循环阶段顺序:timers 阶段之后还有 I/O callbacks、poll、check 等阶段;如果上一阶段耗时久,也会间接推迟 timers 回调的执行时机
setInterval 容易积压,推荐用递归 setTimeout 替代
setInterval 每隔 delay 就往 timers 队列塞一个新回调,不管上一个是否执行完:
- 若回调执行耗时 > delay(例如每 100ms 触发一次,但每次要 150ms),队列就会堆积,后续回调“连发”,失去节奏
- 它不会跳过已积压的任务,也不会自动取消——可能某次卡顿后突然一口气执行五六个回调
- 而递归
setTimeout是“上一次结束 → 立即设置下一次”,天然避免堆积,节奏更可控
Node.js 中 timers 阶段还区分“已到期”和“即将到期”
libuv 使用最小堆管理定时器,根节点永远是最快到期的那个。事件循环在 timers 阶段会:
- 检查堆顶是否已到期;若是,取出执行,并继续检查下一个
- 直到堆顶未到期,或本轮执行达到上限,就退出该阶段
- 这意味着:多个定时器即使 delay 相同,也可能因注册顺序或系统精度,出现细微执行先后


















