宏任务 (MacroTask) 和 微任务 (MicroTask) 有什么区别?
在 JavaScript 的事件循环 (Event Loop) 机制中,宏任务 (MacroTask) 和微任务 (MicroTask) 是处理异步操作的两种不同类型的任务队列。
它们最核心的区别在于 执行时机 和 优先级。
1. 核心区别总结
- 宏任务 (MacroTask):代表一个个独立的、离散的工作单元。浏览器完成一个宏任务后,通常会进行 UI 渲染,然后再执行下一个宏任务。
- 微任务 (MicroTask):代表需要“立即”执行的小任务。它们会在当前宏任务结束后、UI 渲染之前、以及下一个宏任务开始之前全部执行完毕。
一句话口诀: 同步代码先执行,微任务紧跟其后清空队列,最后才轮到下一个宏任务。
2. 常见的任务分类
| 分类 | 浏览器环境常见 API | Node.js 环境常见 API |
|---|---|---|
| 宏任务 (MacroTask) | setTimeoutsetIntervalscript (整体代码)UI 渲染 I/O (网络请求完成等) |
setImmediateI/O 操作 |
| 微任务 (MicroTask) | Promise.then / .catch / .finallyMutationObserver |
process.nextTick (优先级最高)Promise |
3. 事件循环的执行流程
JavaScript 引擎会按照以下步骤循环执行:
- 执行栈 (Call Stack):执行主线程中的同步代码(这本身就是一个宏任务)。
- 清空微任务队列:当同步代码执行完毕(调用栈为空)时,检查微任务队列。如果有微任务,一次性全部执行完(包括在执行微任务过程中产生的新微任务)。
- UI 渲染:浏览器尝试进行页面重绘(如果需要)。
- 执行下一个宏任务:从宏任务队列中取出一个任务执行。
- 回到第 2 步。
4. 代码示例与解析
请看下面的代码,试着预测输出顺序:
javascript
console.log('1. 脚本开始 (同步)');
setTimeout(() => {
console.log('2. setTimeout (宏任务)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise 1 (微任务)');
}).then(() => {
console.log('4. Promise 2 (微任务 - 链式)');
});
console.log('5. 脚本结束 (同步)');
正确输出顺序:
plaintext
1. 脚本开始 (同步)
5. 脚本结束 (同步)
3. Promise 1 (微任务)
4. Promise 2 (微任务 - 链式)
2. setTimeout (宏任务)
详细解析:
同步阶段:
- 执行
console.log('1...')。 - 遇到
setTimeout,将其回调函数放入 宏任务队列。 - 遇到
Promise.then,将其回调放入 微任务队列。 - 执行
console.log('5...')。 - 当前宏任务(Script 整体代码)执行完毕。
- 执行
微任务阶段:
- 检查微任务队列,发现有
Promise 1。执行它,输出3...。 Promise 1执行完后,触发了下一个.then,将Promise 2加入微任务队列。- 队列还没空,继续执行
Promise 2,输出4...。 - 微任务队列清空。
- 检查微任务队列,发现有
宏任务阶段:
- 从宏任务队列中取出
setTimeout的回调。 - 执行,输出
2...。
- 从宏任务队列中取出
5. 生活中的类比
想象你去银行办理业务(你是主线程):
- 宏任务:就像是排队办理业务的客户。柜员(JS 引擎)处理完一个客户(宏任务),可能会喝口水、看下时间(UI 渲染),然后叫下一个号。
- 微任务:就像是当前客户办完业务后,突然说:“啊,稍等,我还要顺便改个密码”。
- 柜员不会让他重新排队(因为那样太慢了)。
- 柜员会立刻帮他把这个小需求处理完(执行微任务),直到该客户所有的小需求都搞定,才会叫下一个客户(下一个宏任务)。
6. 为什么要区分这两者?
- UI 渲染性能:微任务在渲染前执行,意味着你可以在微任务中更新数据,用户不会看到中间状态的闪烁。
- 逻辑连贯性:微任务确保了某些异步操作(如 Promise)能尽可能快地执行,保持逻辑的紧密性,而不需要等待繁重的宏任务队列。