宏任务严格按插入顺序排队,每次只执行队首一个,必须等当前宏任务结束且微任务队列清空后才执行下一个;其由宿主环境(浏览器/node.js)发起,常见来源包括script整体代码、settimeout/setinterval回调、ui事件、i/o回调等。

宏任务在事件循环中不是“插队执行”,而是严格按插入顺序排队,每次只取队首一个执行;它不主动调度,也不嵌套执行,必须等当前宏任务彻底结束、微任务队列清空后,才轮到下一个。
宏任务的来源和排队主体
宏任务不是 JavaScript 引擎自己生成的,而是由宿主环境(浏览器或 Node.js)发起并维护的。常见来源包括:
- script 标签整体代码——这是第一个宏任务,也是事件循环的起点
- setTimeout / setInterval 回调——计时器到期后,回调被推入宏任务队列
- UI 事件(如 click、input、scroll)——用户交互触发,由浏览器调度入队
- I/O 回调(如 fetch 响应、文件读取完成)——系统底层完成后再交由宿主推入队列
- requestAnimationFrame——虽常用于渲染前,但实际归类为宏任务层级(部分浏览器实现中会优先于普通 setTimeout)
宏任务队列不是“单个队列”,而是多源有序容器
宿主环境内部可能维护多个宏任务子队列(比如 DOM 事件队列、定时器队列、网络响应队列),但对外统一表现为“按插入时间顺序”的逻辑队列。关键点是:
- 不同来源的任务插入时,宿主会打上时间戳或按注册顺序排序
- 事件循环每轮只从这个逻辑队首取出一个任务执行,不管它来自哪一类源
- 不存在“高优宏任务插队”机制(不像微任务那样强制立即执行)
- 例如:setTimeout(fn, 0) 和用户点击事件几乎同时发生,谁先注册谁排前面,不是谁“更紧急”谁先执行
宏任务与微任务的交接规则
宏任务本身不负责调度微任务,但它触发了微任务执行的时机窗口。具体交接逻辑是:
- 一个宏任务执行完毕(包括所有同步代码和可能产生的异步注册)
- JS 引擎立刻暂停宏任务流程,转向检查微任务队列
- 只要微任务队列非空,就持续取出队首执行,过程中新加入的微任务也追加到队尾,直到队列完全清空
- 微任务全部执行完,才回到宏任务队列,取下一个任务——哪怕此时又有新宏任务刚被推入
常见误解澄清
几个容易混淆的点需要明确:
- setTimeout(fn, 0) 不等于“立刻执行”——它只是尽快入队,仍要等当前宏任务+全部微任务走完才能轮到
- 宏任务不会“嵌套执行”——比如 setTimeout 里再调 setTimeout,第二个回调是新宏任务,排在当前队列末尾,不会打断正在执行的宏任务
- UI 渲染不是独立宏任务,而是渲染阶段的触发点——浏览器通常在一次宏任务+微任务周期结束后,判断是否需要重绘,这个过程本身不占宏任务名额,但可能触发新的宏任务(如 resize 事件)
- Promise 构造函数内的代码是同步的——new Promise(() => console.log('sync')) 会立即输出,而 .then() 才是微任务











