16. 说说你对宏任务和微任务的理解?
问题解析
宏任务(MacroTask)和微任务(MicroTask)是JavaScript异步编程的核心概念。理解它们的区别、执行顺序以及应用场景,对于掌握JavaScript的异步执行机制至关重要。
核心概念
宏任务(MacroTask)
宏任务是JavaScript中最基础的异步任务类型,由宿主环境(浏览器或Node.js)提供。每个宏任务代表一个完整的执行单元。
特点:
- 时间粒度较大,执行间隔不能精确控制
- 每次事件循环只执行一个宏任务
- 宏任务执行完毕后,会清空微任务队列
微任务(MicroTask)
微任务是在当前宏任务执行结束后、下一个宏任务开始之前执行的任务。微任务的执行优先级高于宏任务。
特点:
- 执行时机在当前宏任务结束后、渲染之前
- 微任务队列会一次性全部执行完毕
- 如果在执行微任务过程中又产生微任务,会继续执行直到队列为空
详细解答
常见的宏任务和微任务
| 类型 | 常见API |
|---|---|
| 宏任务 | script代码、setTimeout、setInterval、I/O、UI渲染、postMessage |
| 微任务 | Promise.then/catch/finally、MutationObserver、queueMicrotask、process.nextTick(Node.js) |
执行顺序示例
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步
// 输出: 1 -> 4 -> 3 -> 2
微任务的优先级
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise1');
});
Promise.resolve().then(() => {
console.log('Promise2');
});
queueMicrotask(() => {
console.log('queueMicrotask');
});
// 输出: Promise1 -> Promise2 -> queueMicrotask -> setTimeout
深入理解
事件循环的完整流程
1. 执行同步代码(这本身是一个宏任务)
2. 执行完同步代码后,检查微任务队列
3. 执行所有微任务(如果执行过程中产生新微任务,继续执行)
4. 微任务队列清空后,执行渲染(如果需要)
5. 执行下一个宏任务
6. 重复步骤2-5
嵌套场景分析
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
// 输出: timeout1 -> promise1 -> timeout2 -> promise2
解析:
- 第一个setTimeout(宏任务)执行,输出timeout1
- 遇到Promise.then,加入微任务队列
- 第一个宏任务结束,执行微任务,输出promise1
- 执行第二个setTimeout(宏任务),输出timeout2
- 遇到Promise.then,加入微任务队列
- 第二个宏任务结束,执行微任务,输出promise2
微任务递归问题
let count = 0;
function addMicroTask() {
Promise.resolve().then(() => {
count++;
console.log('微任务', count);
if (count < 3) {
addMicroTask(); // 递归添加微任务
}
});
}
addMicroTask();
setTimeout(() => {
console.log('宏任务');
}, 0);
// 输出: 微任务 1 -> 微任务 2 -> 微任务 3 -> 宏任务
注意:如果微任务递归没有终止条件,会导致事件循环被阻塞,页面无法渲染和响应。
最佳实践
1. 合理使用微任务
// 使用queueMicrotask显式创建微任务
function doSomething() {
// 做一些同步工作
console.log('同步工作');
// 将后续工作放入微任务
queueMicrotask(() => {
console.log('微任务工作');
});
}
2. 避免微任务阻塞
// 错误:微任务过多会阻塞渲染
function badPractice() {
for (let i = 0; i < 10000; i++) {
Promise.resolve().then(() => {
heavyComputation();
});
}
}
// 正确:使用宏任务让出主线程
function goodPractice() {
function processChunk(i) {
if (i >= 10000) return;
heavyComputation();
// 每处理一部分,让出主线程
if (i % 100 === 0) {
setTimeout(() => processChunk(i + 1), 0);
} else {
processChunk(i + 1);
}
}
processChunk(0);
}
3. 使用MutationObserver
// MutationObserver也是微任务
const observer = new MutationObserver((mutations) => {
console.log('DOM变化', mutations);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 修改DOM
document.body.appendChild(document.createElement('div'));
// 回调会在同步代码执行完毕后立即执行
面试要点
- 执行优先级:同步代码 > 微任务 > 宏任务
- 队列清空:每个宏任务结束后,会清空整个微任务队列
- 产生时机:微任务可以在其他微任务执行过程中产生,会立即执行
- 浏览器渲染:微任务在渲染之前执行,宏任务在渲染之后执行
- 实际应用:Promise使用微任务确保异步回调尽快执行
经典面试题
console.log('start');
setTimeout(() => {
console.log('timeout1');
}, 0);
new Promise((resolve) => {
console.log('promise');
resolve();
}).then(() => {
console.log('then1');
});
Promise.resolve().then(() => {
console.log('then2');
return Promise.resolve();
}).then(() => {
console.log('then3');
});
console.log('end');
// 输出: start -> promise -> end -> then1 -> then2 -> then3 -> timeout1
解析:
- Promise构造函数是同步执行的,所以先输出promise
- then1、then2是同一轮的微任务
- then3虽然返回Promise.resolve(),但在现代浏览器中,then的链式调用会在新的微任务中执行