javascript事件循环中,每个宏任务执行完后必须立即清空所有微任务,之后才执行下一个宏任务;同步代码属首个宏任务,promise.then等微任务在宏任务末尾执行,settimeout回调则排在其后。

JavaScript 的宏任务与微任务交互机制,本质是事件循环中一种严格嵌套、优先级分明的调度规则。
它不是“谁先注册谁先执行”,而是由执行时机和队列清空逻辑决定的:每个宏任务结束后,必须立刻、一次性清空所有已排队的微任务,之后才允许下一个宏任务入场。
微任务总在宏任务“夹缝”中执行
宏任务(如 setTimeout、script 整体、click 事件)代表一个相对完整的工作单元;微任务(如 Promise.then、queueMicrotask、MutationObserver)则是它的“收尾补充”。
关键逻辑:
- 同步代码属于第一个宏任务;
- 它执行完后,不直接跳去下一个宏任务;
- 而是暂停宏任务队列,转头执行全部积攒的微任务,哪怕过程中又产生新的微任务,也一并执行完;
- 微任务队列彻底为空,才从宏任务队列取下一个任务。
这就像快递站派件:
先送完一整车(宏任务),
再把车上附带的加急小包裹(微任务)全部当场分发完毕,
然后才装下一车货。
宏任务之间天然被微任务“隔离”
因为每次宏任务结束都会强制清空微任务队列,所以:
- 微任务永远不会跨宏任务“累积”;
- 上一个宏任务产生的微任务,绝不会拖到下一个宏任务之后执行;
- 即使
setTimeout设为0ms,它的回调也一定排在当前宏任务+所有微任务之后。
例如:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
输出一定是 1 → 4 → 3 → 2,
不是 1 → 4 → 2 → 3,
更不是 1 → 2 → 4 → 3。
原因:1 和 4 是同步代码(第一个宏任务);3 是微任务,插在宏任务末尾;2 是新宏任务,只能等微任务跑完才轮到。
微任务可递归触发,但不会跳出当前“清空周期”
Promise.then 中再 then,或 queueMicrotask 里再调一次 queueMicrotask,新微任务会追加到当前微任务队列尾部,并继续执行——直到队列真正变空。
这意味着:
- 微任务可以形成链式反应,但始终在同一个宏任务出口处完成;
- 不会中断渲染、不打断用户交互,适合做状态收敛、响应统一等精细控制。
浏览器渲染是宏任务间的“观察窗口”
虽然 UI 渲染本身常被归为宏任务,但它并不像 setTimeout 那样显式排队;
浏览器通常在一次宏任务 + 全部微任务执行完毕后,检查 DOM 是否变化,再决定是否触发渲染。
这就解释了为什么:
- 连续多次
Promise.then不会触发多次重绘; - 但连续多次
setTimeout可能对应多次渲染帧(取决于间隔和刷新率)。
微任务因此成为“更新后立即响应、但不触发渲染”的理想载体——比如 Vue/React 的响应式更新队列就基于此机制。
不复杂但容易忽略。










