宏任务执行完后必须清空全部微任务队列,之后才取下一个宏任务;微任务在宏任务出口处强制、不可打断地执行完毕;Promise.then与queueMicrotask同属一轮微任务,按序执行。

宏任务和微任务的优先级抢占逻辑,核心不在“谁快谁慢”,而在于事件循环的硬性调度规则:每个宏任务执行完后,必须清空全部微任务队列,一个都不能留,之后才允许取下一个宏任务。这不是抢占,是强制顺序。
微任务总在宏任务出口处“强制清空”
只要当前宏任务(比如脚本主流程、setTimeout 回调、事件处理函数)执行完毕,JS 引擎立刻暂停宏任务调度,转而逐个执行微任务队列里的所有回调,直到队列为空。这个过程不可打断、不可延迟、不看时间戳——哪怕新进来的微任务是在上一个微任务里刚创建的,也得在这轮清空中执行完。
- Promise.resolve().then(() => console.log('A')) 和 queueMicrotask(() => console.log('B')) 都会进入同一轮微任务队列,按入队顺序执行
- 即使在 setTimeout 回调里写 Promise.then,它的回调仍属于本轮宏任务结束后的微任务,一定早于下一个 setTimeout
- 浏览器渲染(UI rendering)发生在微任务清空之后、下一个宏任务开始之前,所以微任务能影响本次渲染前的状态
常见任务类型的明确归属
识别优先级,先分清哪些是宏任务、哪些是微任务。记准典型代表,比死记分类更可靠:
- 宏任务:script 主流程、setTimeout、setInterval、I/O 回调(如 fetch.then 的上层包装)、DOM 事件(click、input)、requestAnimationFrame(虽时机特殊,但归类为宏任务)、UI 渲染
- 微任务:Promise.then/catch/finally、async/await 中 await 后的代码、queueMicrotask()、MutationObserver 回调
- Node.js 特例:process.nextTick 优先级高于所有微任务,它在当前操作结束后、任何微任务之前执行,不属于标准微任务,但实际效果是“超微任务”
async/await 是微任务的“隐形入口”
await 不是暂停执行,而是把后续代码包装成 Promise.then 回调,因此它天然落入微任务队列。这导致看似连续的语句,实际存在明确的异步断点。
- console.log('start') → 同步
- await Promise.resolve() → 当前宏任务继续,但 await 后面的代码被推入微任务
- console.log('after await') → 微任务,在当前宏任务结束后立即执行
- setTimeout(() => console.log('timeout'), 0) → 宏任务,排在所有微任务之后
requestAnimationFrame 是宏任务里的“特例时间点”
它虽属宏任务,但不进普通宏任务队列,而是由浏览器在下一次重绘前统一触发。执行时机固定在:当前宏任务 + 所有微任务执行完 → 浏览器准备渲染帧之前。这意味着:
- 它不会插在两个微任务之间,也不会抢在 Promise.then 前面
- 连续多次调用 rAF,浏览器只保留最后一次,避免重复排队
- 它适合做动画同步,但不能用来控制异步逻辑顺序——别指望它比 then 先跑

















