JavaScript 跑在单一线程上,一次只能做一件事。同步代码在调用栈上由上而下执行。任何异步的东西——setTimeout 回调、Promise .then、click handler——都不会就地执行,而是被放进队列。事件循环就是决定「下一个跑什么」的简单规则:只要调用栈一空,就取出下一个等待中的任务执行,然后重复。这个工具把这个循环一步一步变得看得见。
其实有两个队列,而它们的优先顺序正是整个谜题的关键。Promise 回调(与 queueMicrotask)进 microtask queue;setTimeout、setInterval 与多数事件进 macrotask queue。规则是:每个 macrotask 之后(以及最初的 script 之后),事件循环会先清空整个 microtask queue,才执行下一个 macrotask。这就是为什么相同延迟下,Promise .then 永远赢过 setTimeout——microtask 会先被排干。看这个工具里的两个队列,顺序就不再是谜。
关于这个主题的常见疑问与实用解答。
因为 Promise 回调是 microtask、setTimeout 回调是 macrotask,而事件循环在每个 macrotask 之后(以及最初 script 之后)会先清空整个 microtask queue,才执行下一个 macrotask。所以即使 setTimeout 设 0ms,也得在 macrotask queue 里等,让所有待处理的 microtask 先跑。
Promise 的反应(.then、.catch、.finally)、await 的后续、queueMicrotask 的回调、MutationObserver 的回调都是 microtask。setTimeout、setInterval、requestAnimationFrame、I/O 与多数 DOM 事件是 macrotask。这个区分重要,因为事件循环会在 macrotask 之间清空所有 microtask,所以若你无止尽地排 microtask,可能饿死画面渲染或稍后的计时器。
不会。0ms 延迟只代表回调「尽快可以跑」,但它仍会进 macrotask queue,等到目前的同步代码与所有待处理 microtask 都结束才执行。浏览器还会把嵌套计时器夹到最小值(几层之后约 4ms),所以 0ms 是下限,不是立即调用。