宏任务队列每次只取一个任务执行,执行后立即清空微任务队列;微任务队列在每个宏任务后无中断地全部执行,且新微任务会加入本轮末尾继续执行。

宏任务队列:每次只取一个,节奏由宿主环境决定
宏任务队列像一条单行道,事件循环每轮只从中取出**第一个**任务执行,比如 setTimeout 回调、setInterval、I/O 完成通知、UI 渲染(浏览器)、甚至最开始的整个 script 脚本本身。执行完这个任务后,主线程不会继续取第二个宏任务,而是立刻转向微任务队列。
关键点在于:宏任务之间天然存在“间隔”。这个间隔不是空闲,而是被微任务填满——也就是说,两个宏任务之间必然夹着一次完整的微任务清空过程。这种节奏决定了页面响应、动画帧插入、用户点击反馈等时机,是浏览器可感知性能的底层节拍器。
- setTimeout(fn, 0) 不代表“立刻”,只是尽快排进宏任务队列末尾,要等前面所有宏任务跑完、且本轮微任务清空后才轮到
- requestAnimationFrame 属于宏任务,但它被浏览器特殊调度,通常在下一帧渲染前执行,优先级高于普通 setTimeout
- 多个 setTimeout 嵌套或并发注册,会按注册顺序+延迟时间共同决定在宏任务队列中的实际位置
微任务队列:每轮宏任务后立即清空,无中断、不插队
微任务队列更像是一个“即时处理箱”。只要它非空,JS 引擎就会持续从中取任务执行,直到彻底清空。这个过程发生在**每个宏任务执行完毕之后、下一个宏任务开始之前**,而且中间不会穿插任何渲染或其它宏任务。
Promise.then、queueMicrotask、MutationObserver 回调都进这个队列。它们没有“优先级高低”之分,只有“先进先出”;但有一个重要特性:微任务内部如果再产生新微任务(比如在 then 里又写一个 then),这个新任务会被追加到当前队列末尾,并在本轮继续执行——不会等到下一轮事件循环。
立即学习“Java免费学习笔记(深入)”;
将后端 Java 接口文档转换为前端 TypeScript 类型定义(interface / type)。输入:飞书文档、Spring Boot 接口文档、Swagger/OpenAPI、手写 API 文档等。输出:包含请求参数与响应结果的 .md 文件,含完整 JSDoc 注释。触发关键词:Java 转 TS、接口转换、生成前端类型、接口文档转 TypeScript、DTO/VO/BO 转 interface、后端文档转前端类型、生成接口类型、类型定义生成。
- Promise.resolve().then(() => { console.log(1); Promise.resolve().then(() => console.log(2)); }); 输出一定是 1 然后 2,中间不会被 setTimeout 打断
- queueMicrotask 和 Promise.then 在执行时机上完全等价,只是语义更直接、无状态转换开销
- MutationObserver 回调虽然也是微任务,但它的触发依赖 DOM 变更批量汇总,所以实际执行时机略滞后于 Promise,但仍属于同一轮清空
多轮事件循环中两队列的状态流转
假设初始同步代码执行完毕,此时调用栈为空。事件循环开始第一轮:
- 检查微任务队列:若有任务,全部执行(可能新增更多微任务,也一并执行完)
- 检查宏任务队列:取第一个执行(比如一个 setTimeout 回调)
- 该宏任务执行完毕 → 再次检查微任务队列:清空所有现存任务(包括它自己产生的)
- 再取下一个宏任务 → 重复上述“宏→微→宏→微”链条
注意:微任务队列在每轮之间总是空的,因为上一轮已强制清空;而宏任务队列可以持续堆积,比如连续 10 个 setTimeout,它们会依次在后续 10 轮中被执行,每轮之间都夹着一次微任务清空。
容易误判的典型场景
很多人以为“微任务比宏任务快”,其实更准确的说法是:“微任务总在宏任务的缝隙里集中执行”。例如下面这段代码:
console.log('a');setTimeout(() => console.log('b'), 0);
Promise.resolve().then(() => console.log('c'));
console.log('d');
输出是 a d c b。不是因为 c “抢在” b 前面,而是因为:a/d 是同步;c 是微任务,紧接在同步后执行;b 是宏任务,在整个 script(首个宏任务)跑完、且微任务清空后,才从队列里被取出执行。

















