javascript 事件循环中,多个 settimeout 会被统一放入宏任务队列(macrotask queue)进行顺序管理。浏览器或 node.js 环境会按照代码执行的先后顺序将定时器注册到队列中,并在当前调用栈清空且满足最小延迟时间后,依次取出执行。这意味着即使设置了不同的延迟时间,如果前一个任务耗时较长,后续任务的执行也会被顺延。

多个 setTimeout 的入队顺序与执行时机
当你在同步代码中连续调用多个 setTimeout 时,它们会严格按照调用的顺序进入宏任务队列。每个定时器都有一个关联的回调函数和设定的延迟时间。事件循环机制会在主线程空闲时检查队列,优先处理那些等待时间最长的任务,但本质上它们是排队等待执行的。

console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
上述代码的执行结果如下:
Start
End
Timeout 1
Timeout 2
立即学习“Java免费学习笔记(深入)”;
在这个例子中,两个 setTimeout 都设置了 0 毫秒延迟。由于它们是在同步代码块中按顺序注册的,因此它们的回调在宏任务队列中的顺序也是确定的。主线程先执行同步代码打印 'Start' 和 'End',然后事件循环从宏任务队列中取出第一个回调执行,再取出第二个。这证明了多个 setTimeout 是作为独立的宏任务被管理和执行的。
延迟时间对执行顺序的影响
虽然 setTimeout 的延迟时间决定了任务何时可以被加入执行队列,但它并不保证绝对的准时执行。如果前一个宏任务执行时间很长,或者当前有更高优先级的任务,后续的 setTimeout 即使延迟时间较短,也需要等待前面的任务完成。此外,浏览器会对嵌套过深的定时器进行节流,最小延迟通常不低于 4 毫秒。

console.log('Start');
setTimeout(() => {
console.log('Timeout A');
}, 10);
setTimeout(() => {
console.log('Timeout B');
}, 5);
console.log('End');
预期输出:
Start
End
Timeout B
Timeout A
尽管 Timeout B 的延迟时间更短(5ms vs 10ms),但在实际运行中,由于两者都非常接近,且主线程同步代码执行极快,事件循环通常会先唤醒等待时间较短的任务。然而,这种顺序并非绝对可靠,特别是在高负载环境下。关键在于理解它们都是宏任务,必须等待调用栈清空。如果我们将其中一个改为同步阻塞操作,情况就会改变。
长时间运行的同步代码如何影响 setTimeout
如果在 setTimeout 之前有一段耗时的同步代码,它会阻塞主线程,导致所有已注册的定时器都无法及时执行。这是因为 JavaScript 是单线程的,只有当同步代码执行完毕,控制权交还给事件循环后,才会去检查宏任务队列。因此,setTimeout 的实际触发时间 = 设定延迟 + 主线程阻塞时间。
console.log('Start');
// 模拟长时间运行的同步任务
for (let i = 0; i < 1000000000; i++) {}
setTimeout(() => {
console.log('Delayed Task');
}, 0);
console.log('End');
预期输出:
Start
[长时间阻塞]
End
Delayed Task
在这个示例中,即使 setTimeout 设置为 0 毫秒延迟,它也必须等待巨大的同步循环结束后才能执行。这清晰地展示了宏任务队列的管理机制:它不抢占主线程,而是被动等待。开发者应避免在主线程执行耗时操作,以确保定时器的及时性。


















