setTimeout和setInterval是宏任务,回调在事件循环的timers阶段执行,不保证精确时间,需等待同步代码和微任务完成,且受主线程阻塞、浏览器节流或Node.js阶段调度影响。

JS 定时器函数(setTimeout 和 setInterval)是事件循环中宏任务(MacroTask)的主要来源之一,它们不直接执行代码,而是把回调“预约”进事件循环的特定阶段,由浏览器或 Node.js 的定时器模块统一调度。
定时器是宏任务的触发入口
每次调用 setTimeout 或 setInterval,JavaScript 引擎会将回调函数和延迟时间注册到底层定时器系统(如浏览器的 timer thread 或 Node.js 的 timers 阶段最小堆)。当延迟时间到达,并不意味着立刻执行——它只是具备了“可执行资格”,真正执行要等到:
- 当前同步代码全部运行完毕;
- 所有微任务(如 Promise.then、queueMicrotask)清空;
- 事件循环进入下一个宏任务阶段(比如浏览器的“timer phase”或 Node.js 的 timers 阶段)。
它们不保证精确执行时间
定时器设定的毫秒数只是“最早可执行时间”,不是截止时间。实际执行可能延后,常见原因包括:
- 主线程正忙于执行长同步任务(如大数组遍历、复杂计算),阻塞事件循环;
- 浏览器节流策略:标签页非激活时,
setTimeout最小间隔被拉长至 1000ms; - Node.js 中 timers 阶段受前一阶段(如 poll)耗时影响,若 poll 阶段有大量 I/O 回调未处理完,timers 阶段会被跳过一轮。
与微任务的执行顺序很关键
在同一个事件循环周期里,微任务总是在宏任务之后、下一轮宏任务之前执行。这意味着:
-
setTimeout(() => console.log('macro'), 0)一定晚于Promise.resolve().then(() => console.log('micro')); - 连续多次
setTimeout(fn, 0)会排队成多个宏任务,彼此之间至少隔一个事件循环周期; - 想让某段逻辑“尽快但不打断当前同步流”,用
queueMicrotask比setTimeout(..., 0)更合适。
在 Node.js 中有明确的阶段归属
Node.js 的事件循环把定时器严格限定在 timers 阶段 执行:
- 该阶段只处理已到期的
setTimeout/setInterval回调; - 即使设为 0ms,也必须等上一轮循环完成、进入新 timers 阶段才执行;
- 它和
setImmediate(check 阶段)、process.nextTick(不属于事件循环阶段,而是紧贴当前操作末尾)有清晰的优先级分层。


















