宏任务(MacroTask)与微任务(MicroTask)是 JavaScript 异步执行机制中的核心概念,直接影响代码的执行顺序和性能。以下是从定义到实战的系统性解析:
一、核心定义与区别
1. 宏任务(MacroTask)
- 定义:由浏览器或 Node.js 环境提供的异步任务,每次事件循环(Event Loop)开始时执行。
- 常见类型:
- 浏览器环境:
setTimeout
、setInterval
、setImmediate
(Node.js)、requestAnimationFrame
、UI渲染
。 - Node.js 环境:
setTimeout
、setInterval
、I/O操作
(如文件读取)。
- 浏览器环境:
2. 微任务(MicroTask)
- 定义:由 JavaScript 引擎内部提供的异步任务,在当前宏任务执行结束后、下一个宏任务开始前立即执行。
- 常见类型:
Promise.then()
、Promise.catch()
、Promise.finally()
MutationObserver
(DOM 监听)process.nextTick
(Node.js,优先级高于其他微任务)
二、执行机制:事件循环中的优先级
- 事件循环(Event Loop)每次从宏任务队列中取一个任务执行。
- 宏任务执行完毕后,立即清空当前的微任务队列(所有微任务按顺序执行)。
- 微任务队列清空后,进行UI 渲染(如果有 DOM 变更)。
- 进入下一轮事件循环,重复上述步骤。
关键点:微任务的执行时机优先于下一个宏任务。
三、代码示例:理解执行顺序
javascript
console.log('1'); // 同步任务,立即执行
setTimeout(() => {
console.log('2'); // 宏任务,放入宏任务队列
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务,放入微任务队列
});
console.log('4'); // 同步任务,立即执行
// 输出顺序:1 → 4 → 3 → 2
执行流程解析:
- 同步代码
console.log('1')
和console.log('4')
依次执行。 setTimeout
的回调被放入宏任务队列。Promise.then
的回调被放入微任务队列。- 同步代码执行完毕后,检查微任务队列,执行
console.log('3')
。 - 微任务队列清空后,进入下一轮事件循环,执行宏任务队列中的
console.log('2')
。
四、进阶示例:复杂嵌套场景
javascript
console.log('start');
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => console.log('promise1'));
}, 0);
Promise.resolve().then(() => {
console.log('promise2');
setTimeout(() => console.log('setTimeout2'), 0);
});
console.log('end');
// 输出顺序:start → end → promise2 → setTimeout1 → promise1 → setTimeout2
执行流程解析:
- 同步代码
console.log('start')
和console.log('end')
执行。 - 第一个
setTimeout
的回调放入宏任务队列。 Promise.then
的回调放入微任务队列,当前宏任务结束后执行console.log('promise2')
。- 微任务中第二个
setTimeout
的回调放入宏任务队列。 - 进入下一轮事件循环,执行第一个宏任务
console.log('setTimeout1')
。 - 第一个宏任务结束后,检查微任务队列,执行
console.log('promise1')
。 - 进入下一轮事件循环,执行第二个宏任务
console.log('setTimeout2')
。
五、性能优化中的应用
1. 批量 DOM 操作
- 问题:频繁操作 DOM 会触发多次重排(reflow)和重绘(repaint)。
- 优化:使用微任务批量处理 DOM 变更。
javascript
// 错误示例:多次触发重排
document.body.append(el1);
document.body.append(el2);
document.body.append(el3);
// 优化示例:用微任务批量处理
Promise.resolve().then(() => {
document.body.append(el1);
document.body.append(el2);
document.body.append(el3);
}); // 仅触发一次重排
2. 避免 UI 卡顿
- 反例:在宏任务中执行大量计算,阻塞 UI 渲染。
javascript
// 阻塞主线程2秒,页面无响应
setTimeout(() => {
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
console.log(sum);
}, 0);
- 优化:用微任务分段执行,保持页面响应性。
javascript
function heavyTask() {
let sum = 0;
const batchSize = 1e6;
let i = 0;
function processBatch() {
for (let j = 0; j < batchSize && i < 1e9; j++, i++) {
sum += i;
}
if (i < 1e9) {
Promise.resolve().then(processBatch); // 用微任务继续执行
} else {
console.log(sum);
}
}
processBatch();
}
六、Node.js 与浏览器的差异
特性 | 浏览器环境 | Node.js 环境 |
---|---|---|
微任务队列 | 所有微任务按注册顺序执行。 | process.nextTick 优先于其他微任务(如Promise.then )。 |
宏任务类型 | 包含requestAnimationFrame 、UI 渲染等。 | 包含setImmediate 、I/O回调 等。 |
事件循环阶段 | 无明确阶段划分。 | 分为timers 、I/O callbacks 、idle, prepare 等阶段。 |
七、高频面试考点
-
手写执行顺序题
给出包含宏任务和微任务的代码,要求写出输出顺序。关键是记住:- 同步代码优先执行
- 微任务在当前宏任务结束后立即执行
- 宏任务在下一轮事件循环开始时执行
-
微任务的典型应用
- 实现
Promise
的链式调用 Vue 3
的响应式更新机制(queueMicrotask
)
- 实现
-
性能优化
- 用微任务合并 DOM 操作,减少重排重绘
- 避免在微任务中执行大量计算,防止阻塞 UI 渲染
八、一句话总结
- 宏任务:异步操作的 “大块头”,每次事件循环开始时执行(如定时器、网络请求)。
- 微任务:异步操作的 “插队者”,在当前任务结束后立即执行(如 Promise 回调)。
合理利用宏微任务的执行顺序,是写出高性能、无卡顿前端应用的关键。