Node.js的事件循环是其处理异步操作的核心机制,整体分为多个不同的阶段,按照固定顺序循环执行,其中timers阶段是事件循环的第一个阶段,承担着定时器任务调度的重要职责。

timers阶段的核心作用
timers阶段的主要功能是执行所有到期的定时器回调函数,这里提到的定时器指的是通过setTimeout和setInterval创建的定时任务。需要注意的是,timers阶段判断定时器是否到期,依据的是定时器设置的阈值,而不是精确的到期时间,因为系统调度和其他回调的执行耗时都可能导致定时器回调的实际执行时间比预设时间稍晚。
timers阶段的执行规则
在事件循环进入timers阶段时,会先检查当前时间,然后遍历所有已设置的定时器,执行所有阈值小于等于当前时间偏移量的定时器回调,具体规则如下:
- 定时器的阈值是从事件循环开始运行时计算的相对时间,比如
setTimeout(fn, 100)表示事件循环启动后100毫秒执行回调 - 如果某个定时器的阈值还没到,timers阶段会跳过该定时器,继续执行后续阶段
- 同一个timers阶段中,到期的定时器回调会按照设置的顺序依次执行,执行过程中新创建的定时器不会在当前timers阶段被处理
timers阶段与其他阶段的关联
事件循环的阶段顺序是固定的:timers阶段 → pending callbacks阶段 → idle/prepare阶段 → poll阶段 → check阶段 → close callbacks阶段,一轮循环结束后会回到timers阶段重新开始。如果timers阶段没有到期的定时器,事件循环会直接进入下一个阶段,不会在timers阶段阻塞等待。
代码示例验证timers阶段行为
下面通过一个简单的示例来观察timers阶段的执行逻辑:
const start = Date.now();
// 设置100毫秒后执行的定时器
setTimeout(() => {
console.log(`第一个定时器执行,耗时:${Date.now() - start}ms`);
}, 100);
// 设置0毫秒后执行的定时器
setTimeout(() => {
console.log(`第二个定时器执行,耗时:${Date.now() - start}ms`);
// 在回调中执行耗时操作
const begin = Date.now();
while (Date.now() - begin < 50) {} // 阻塞50毫秒
}, 0);
// 设置200毫秒后执行的定时器
setTimeout(() => {
console.log(`第三个定时器执行,耗时:${Date.now() - start}ms`);
}, 200);执行上述代码后,输出结果大致如下:
- 第二个定时器执行,耗时:1ms(因为0毫秒阈值几乎立即到期)
- 第一个定时器执行,耗时:101ms(接近预设的100毫秒,因为第二个回调阻塞了50毫秒,导致第一个定时器实际执行时间延后)
- 第三个定时器执行,耗时:201ms(同样受前面回调执行耗时影响,比预设的200毫秒稍晚)
这个结果也验证了timers阶段不会严格按照预设时间精准执行回调,而是受其他任务执行耗时的影响,只保证在阈值到达后尽快执行。
常见问题说明
为什么setTimeout(fn, 0)不是立即执行?因为事件循环进入timers阶段时才会检查定时器是否到期,而0毫秒的定时器虽然阈值很小,但也需要等待事件循环运行到timers阶段才会被处理,同时如果前面有其他同步代码或者同阶段的其他回调执行,也会进一步延后执行时间。
另外需要注意,setImmediate的回调是在check阶段执行的,和timers阶段的定时器回调属于不同的执行阶段,两者的执行顺序需要结合具体的调用场景判断,不能默认认为定时器回调一定先于setImmediate回调执行。