首先来探讨宏任务的队列调度机制。在 JavaScript 事件循环中,宏任务遵循先进先出(FIFO)的排队原则,但有一个重要前提:当前执行栈必须完全清空,并且微任务队列也需全部处理完毕,之后才会从宏任务队列中取出下一个任务执行。常见的宏任务来源包括:setTimeout、setInterval、setImmediate(仅 Node.js 环境)、I/O 回调、UI 渲染,以及 MessageChannel 的 onmessage 回调。

JavaScript 的宏任务(Macrotask)一旦被创建,事件循环便会将其放入对应的队列中。只有当当前执行栈清空且微任务队列全部执行完毕后,事件循环才会依次从宏任务队列中取出每个任务并执行。
宏任务的常见来源
以下操作都会创建新的宏任务,并加入队列:
- setTimeout 和 setInterval 的回调函数(即使延时设置为 0,它们也属于宏任务)
- setImmediate(仅在 Node.js 环境中可用)
- I/O 操作完成后的回调(例如文件读取、网络请求响应等)
- UI 渲染(浏览器中的一次渲染帧可视为一个宏任务的边界)
- MessageChannel 的
port.onmessage回调(注意:虽然常被用来模拟微任务,但其本质仍是宏任务)
宏任务入队的时机与规则
宏任务并非“立即执行”,而是满足条件后由调度器插入队列:
- 定时器到期时(例如
setTimeout达到指定延迟),回调函数被加入宏任务队列(并非立即执行) - 异步操作完成(如
fetch响应、readFile结束),其then/catch回调中的非 Promise 链部分(即外层回调)属于宏任务 - 宏任务队列遵循先进先出(FIFO)结构,但不同来源可能拥有独立的子队列(例如 Chrome 中
setTimeout与 I/O 回调可能分属不同队列,但对外表现为统一的调度顺序) - 宏任务只有在当前执行栈为空且所有微任务都执行完毕后,才会从队列头部取出一个任务执行
宏任务与微任务的关键区别
宏任务和微任务(例如 Promise.then、queueMicrotask)共同存在于事件循环中,但它们的优先级差异显著:
- 每次宏任务执行完毕后,事件循环会立即清空整个微任务队列(直到没有新的微任务加入)
- 宏任务之间必然穿插一次完整的微任务清空过程
- 微任务不会被新来的宏任务“插队”;新的宏任务只能等待当前宏任务以及所有微任务全部结束后才能执行
简单验证方式
通过嵌套的 setTimeout 和 Promise 即可观察执行顺序:
console.log('1');setTimeout(() => console.log('2'), 0);Promise.resolve().then(() => console.log('3'));console.log('4');// 输出:1 → 4 → 3 → 2执行顺序非常清晰:同步代码(1 和 4)首先执行;随后是微任务(3);最后才轮到宏任务队列中的 setTimeout 回调(2)。
