宏任务(Macro Task)
定义
- 由浏览器或Node.js环境发起的任务,代表需要较长时间执行的代码块。
- 执行时机:在事件循环的每一轮(一个Tick)中执行一个宏任务,之后处理所有微任务。
常见类型
setTimeout
/setInterval
:定时器回调。- I/O操作:文件读写、网络请求等。
- DOM事件回调:如点击事件、滚动事件。
requestAnimationFrame
(浏览器):动画帧回调(通常视为宏任务,但部分资料归类不同)。- 整体脚本代码:
<script>
标签内的同步代码。 postMessage
/MessageChannel
:跨文档通信。
微任务(Micro Task)
定义
- 需要更快执行的高优先级任务,通常与Promise相关。
- 执行时机:在当前宏任务执行完毕后、下一个宏任务开始前,立即清空微任务队列。
常见类型
Promise.then
/Promise.catch
/Promise.finally
:Promise链中的回调。async/await
:底层基于Promise,await
后的代码视为微任务。MutationObserver
(浏览器):监听DOM变化的回调。queueMicrotask
:显式添加微任务的API。process.nextTick
(Node.js):优先级高于其他微任务。
宏任务 (macrotask,简称task) | 微任务 (microtask,简称jobs) |
---|---|
setTimeout | Promise(then catch finally) |
setInterval | MutationObserver |
requestAnimationFrame | queueMicrotask |
setImmediate | process.nextTick(node) |
MessageChannel | |
I/O,事件 | |
script(整体代码块) |
执行顺序规则
事件循环流程:
- 执行一个宏任务(如整体script脚本),同步代码优先,所有同步代码立即执行
- 执行所有微任务(直到微任务队列为空)。
- 可能进行UI渲染(浏览器)。
- 进入下一轮事件循环,执行下一个宏任务。
关键特点:
- 事件循环每次只处理一个宏任务,再处理其产生的微任务队列。所以,微任务队列会在 每个宏任务执行后 立即执行
- 微任务队列(micro task queue)优先级高于宏任务队列。
- 微任务执行期间新添加的微任务会继续执行,直到队列清空。
浏览器环境script脚本执行顺序
- 执行Script整体同步宏任务代码,直到调用栈call stack清空;同时遇到异步任务源(setTimeOut,Promise等)执行,委托给宿主环境监听(setTimeOut延迟时间是否到达,Promise是否reolve),添加到对应的宏和微任务队列
- 执行所有微任务micro task队列任务,直到清空
- 渲染UI,
- 执行异步宏任务macro-task队列的一个任务
- 跳到步骤2,不停循环执行
- 事件循环Event Loop中,每一次循环操作称为 tick
示例对比
示例1:基础执行顺序
console.log("Script Start"); // 宏任务(整体代码)
setTimeout(() => {
console.log("setTimeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("Promise"); // 微任务
});
console.log("Script End"); // 宏任务
输出顺序:
Script Start → Script End → Promise → setTimeout
示例2:微任务嵌套微任务
Promise.resolve().then(() => {
console.log("Micro 1");
Promise.resolve().then(() => {
console.log("Micro 2"); // 新微任务,继续执行
});
});
setTimeout(() => {
console.log("Macro 1");
}, 0);
输出顺序:
Micro 1 → Micro 2 → Macro 1
示例3:宏任务中产生微任务
setTimeout(() => {
console.log("Macro 1");
Promise.resolve().then(() => {
console.log("Micro 1");
});
}, 0);
setTimeout(() => {
console.log("Macro 2");
}, 0);
输出顺序:
Macro 1 → Micro 1 → Macro 2
示例4:大合集
console.log("Script Start"); // 同步代码
setTimeout(() => {
console.log("setTimeout 1"); // 宏任务
Promise.resolve().then(() => {
console.log("Promise in setTimeout 1"); // 微任务
});
}, 0);
Promise.resolve().then(() => {
console.log("Promise 1"); // 微任务
queueMicrotask(() => {
console.log("Microtask in Promise 1"); // 微任务
});
});
setTimeout(() => {
console.log("setTimeout 2"); // 宏任务
queueMicrotask(() => {
console.log("Microtask in setTimeout 2"); // 微任务
});
}, 0);
queueMicrotask(() => {
console.log("Microtask 1"); // 微任务
});
console.log("Script End"); // 同步代码
执行顺序详解:
同步代码执行:
Script Start → Script End
处理微任务队列:
- 微任务队列初始顺序:
[Promise 1, Microtask 1]
执行
Promise 1
:Promise 1
新增微任务
Microtask in Promise 1
,队列变为:[Microtask 1, Microtask in Promise 1]
执行
Microtask 1
:Microtask 1
执行
Microtask in Promise 1
:Microtask in Promise 1
- 微任务队列初始顺序:
执行宏任务队列:
第一个宏任务
setTimeout 1
:setTimeout 1
新增微任务
Promise in setTimeout 1
,立即处理:Promise in setTimeout 1
第二个宏任务
setTimeout 2
:setTimeout 2
新增微任务
Microtask in setTimeout 2
,立即处理:Microtask in setTimeout 2
常见误区
误区1:认为微任务会在所有宏任务之后执行。
- 纠正:微任务在 每个宏任务执行后 立即执行。
误区2:认为嵌套的宏任务会立即执行。
- 纠正:嵌套的宏任务会被添加到队列末尾,按顺序执行。
输出顺序:
> Script Start
> Script End
> Promise 1
> Microtask 1
> Microtask in Promise 1
> setTimeout 1
> Promise in setTimeout 1
> setTimeout 2
> Microtask in setTimeout 2
常见问题与场景
1. 为什么微任务优先于宏任务?
- 设计目标:微任务用于处理高优先级更新(如Promise状态变更),确保在渲染前完成数据更新,避免UI不一致。
2. requestAnimationFrame
是宏任务吗?
- 归类争议:通常认为其回调在渲染前执行,但具体实现可能因浏览器而异,建议单独处理。
3. UI渲染何时进行?
- 时机:在微任务队列清空后、下一轮宏任务前,浏览器可能选择是否渲染。
总结对比表
特性 | 宏任务(Macro Task) | 微任务(Micro Task) |
---|---|---|
执行时机 | 事件循环的每一轮中执行一个 | 当前宏任务结束后立即全部执行 |
优先级 | 低 | 高 |
常见类型 | setTimeout 、I/O、DOM事件 | Promise.then 、MutationObserver |
队列处理 | 单次循环处理一个 | 单次循环处理全部 |
嵌套任务影响 | 新任务进入下一轮循环 | 新任务继续在当前循环执行 |
实际应用建议
- 优化性能:高频操作(如数据更新)优先使用微任务,减少UI卡顿。
- 避免阻塞:长时间运行的微任务会阻塞渲染,需合理拆分任务。
- 框架中的应用:Vue的异步更新队列、React的state批处理均依赖微任务机制。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。