⭐ 推荐:社区规则条款 V1.0

js setTimeout 执行顺序问题

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-21 01:45 ·9 浏览 ·0 回复
内容摘要

setTimeout 回调属于宏任务,需等同步代码和微任务队列清空后执行,因此晚于 Promise.then;同延迟定时器按注册顺序执行,嵌套超过5层延迟至少4ms,长任务和后台节流会导致不精确,不宜用于动画。

setTimeout 的回调属于宏任务(macrotask),只有当前同步代码全部执行完、并且微任务队列(Promise.then、queueMicrotask、MutationObserver)被清空之后才会执行。所以 setTimeout(fn, 0) 永远排在 Promise.resolve().then(fn) 之后,而多个延迟相同的定时器在 Chrome 和 Node 中按注册顺序执行。想用 setTimeout(fn, 0) 抢占执行顺序是不可靠的。

setTimeout(fn, 0) 为什么排在 Promise.then 后面?

结论:因为 setTimeout 注册的是宏任务,Promise.then 注册的是微任务,而事件循环的规则是“每执行完一个宏任务,就清空一次微任务队列”。

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// 输出:sync → promise → timeout

执行过程是:先执行整段同步代码打印 sync;同步代码结束后检查微任务队列,打印 promise;然后才进入定时器阶段,打印 timeout。即使写 setTimeout(fn, -1) 或 setTimeout(fn)(不传延迟),结果也一样,因为延迟值会被规范化成 0。

多个 setTimeout 谁先执行?

结论:多个相同延迟的定时器按注册顺序(进入定时器列表的顺序)执行;延迟不同的按到期时间先后执行。但同一时刻到期的多个定时器,规范本身不保证顺序,只保证“不早于”指定时间。

setTimeout(() => console.log(1), 0);
setTimeout(() => console.log(2), 0);
// 1 2

setTimeout(() => console.log('A'), 100);
setTimeout(() => console.log('B'), 0);
// B A

一个常见的误区是认为后注册的会被“插入”到前面。实际上,在典型实现中,每个定时器带一个到期时间戳,到期时间相同时才使用注册序号进行稳定排序;如果两个定时器都在同一轮事件循环到期,顺序由注册顺序决定。

setTimeout 的最小延迟是多少?

结论:HTML 规范规定,当定时器的嵌套层级大于 5 层时,延迟小于 4ms 的一律提升为 4ms,所以 setTimeout(fn, 0) 的真实延迟是 1~4ms,不是 0。

function nest(n) {
  if (n === 0) return;
  setTimeout(() => { console.log(n); nest(n - 1); }, 0);
}
nest(10);

前 5 次嵌套时,延迟接近 1ms;从第 6 层开始被强制到 4ms。另外,页面切到后台标签页后,Chrome 会把定时器最小间隔节流到 1000ms;隐藏超过 5 分钟还会进入更严格的节流模式(大约每分钟执行一次)。因此,不要用长链 setTimeout 做动画,用 requestAnimationFrame。

定时器为什么不精确,延迟被拉长了怎么办?

结论:定时器的回调不会打断正在执行的代码,只有主线程空闲下来才能执行,所以同步长任务会把所有到期定时器一起推后。

setTimeout(() => console.log('timeout'), 10);
const start = Date.now();
while (Date.now() - start < 200) {} // 阻塞 200ms
// timeout 在约 200ms 后才打印

排查思路有三条:用 Chrome DevTools Performance 面板看长任务(超过 50ms 的任务);把大循环拆成多个 setTimeout 或 requestIdleCallback 分片;需要稳定帧率时改用 requestAnimationFrame。注意,setInterval 也有同样问题,而且会累积漏掉的执行,通常的做法是用 setTimeout 自调用替代。

Node.js 里 setTimeout 的顺序为什么和浏览器不一样?

结论:Node 里 setTimeout(fn, 0) 会被强制改成 1ms,微任务中 process.nextTick 的优先级高于 Promise.then,而 setTimeout(fn, 0) 与 setImmediate 的先后不确定。

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

这段代码在 Node 中可能输出 timeout, immediate,也可能输出 immediate, timeout,取决于进程启动耗时是否超过 1ms。但在 I/O 回调内部,setImmediate 总是先于 setTimeout,因为 I/O 回调属于 poll 阶段,之后进入 check 阶段执行 setImmediate,再下一轮才回到 timers 阶段。

需要保证顺序时该怎么做?

结论:不要靠 setTimeout 的排序做业务顺序控制,改用具名顺序保证的 Promise 链或 async/await。

// 不可靠
setTimeout(stepA, 0);
setTimeout(stepB, 0);

// 可靠
Promise.resolve().then(stepA).then(stepB);
// 或
await stepA(); await stepB();

如果必须延迟执行,用一个定时器串行调度下一步,而不是同时注册多个;需要“尽快但让出主线程”时,用 queueMicrotask(微任务,早于任何宏任务)或 MessageChannel(宏任务,比 setTimeout 少 4ms 的钳制,常用于 polyfill 中模拟 setImmediate)。

总结:setTimeout 的执行顺序由“宏任务与微任务的层级关系、到期时间、注册顺序”三者决定,最小延迟受 4ms 钳制和后台节流约束,延迟精度受主线程阻塞影响。做顺序控制时,用 Promise 或串行调度,比调整延迟值更可靠。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2334.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~