理解宏任务与微任务,是掌握浏览器事件循环执行顺序的核心。简单来说,异步任务在浏览器中分为两类:宏任务(MacroTask)和微任务(MicroTask)。它们的执行时机不同,直接决定了代码的最终输出顺序。
两类任务的具体分类
宏任务(MacroTask)
通常由浏览器宿主环境直接触发,代表一个相对独立的“工作单元”。常见的有:
- 整体
<script>标签代码 setTimeout/setInterval回调- I/O 操作(如文件读取、网络请求
fetch回调) - UI 交互事件(点击、滚动等)
requestAnimationFrame(严格说在渲染帧中,稍后会说明)setImmediate(Node.js 环境,浏览器不支持)
微任务(MicroTask)
通常是由语言层面(ECMAScript 规范)或某些特定 API 触发的,追求尽快执行,优先级比宏任务更高。常见的有:
Promise.then()/.catch()/.finally()async/await函数中await后的代码(本质是Promise微任务)MutationObserver回调queueMicrotask()APIprocess.nextTick(Node.js 环境独有,优先级甚至高于Promise,此处不详述)
执行顺序:一个宏任务 → 清空所有微任务 → 渲染?→ 下一个宏任务
事件循环的每一轮(tick)遵循这样的基本流程:
- 取出一个宏任务执行(例如:执行全局脚本、一个
setTimeout回调)。 - 执行过程中可能产生新的微任务,它们会被追加到同一个微任务队列。
- 宏任务执行完毕,立即检查微任务队列。
- 只要微任务队列不为空,就会依次执行所有微任务,并且在此过程中新添加的微任务也会在本次清空周期内被执行(直到队列清空)。
- 微任务清空后,浏览器可能会根据情况进行一次页面渲染(并非每个宏任务后都会渲染,通常由浏览器调度决定,比如是否满足 16.6ms 的刷新率)。
- 渲染完成后,从宏任务队列中取出下一个宏任务,重复上述过程。
关键结论:微任务永远在当前宏任务结束之后、下一个宏任务开始之前执行,且会一次性清空。因此微任务的优先级高于宏任务。
经典代码示例
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出顺序:1 → 4 → 3 → 2
分析过程:
- 整体
<script>作为第一个宏任务开始。 - 同步代码
console.log('1')立即执行 → 输出 1。 setTimeout将回调登记为宏任务,放入下一轮的宏任务队列。Promise.then将回调登记为微任务,放入当前微任务队列。- 同步代码
console.log('4')立即执行 → 输出 4。 - 第一个宏任务执行完毕,开始清空微任务。
- 执行
then回调 → 输出 3。 - 微任务队列清空,本轮事件循环结束,浏览器(可能)进行渲染。
- 下一轮事件循环开始,取出
setTimeout宏任务执行 → 输出 2。
微任务的“连锁触发”特性
如果在微任务中再次添加微任务,事件循环会在当前周期内一直执行,直到队列彻底清空。这可能导致主线程被长时间占用,页面无法及时渲染。
setTimeout(() => console.log('宏任务'), 0);
function loopMicro() {
Promise.resolve().then(() => {
console.log('微任务');
loopMicro(); // 无限添加微任务
});
}
loopMicro();
上述代码会无限输出“微任务”,宏任务永远无法执行,页面也会陷入假死状态。理解这一点,有助于避免在微任务中递归调用产生性能问题。
结合 DOM 渲染的观察
虽然“每个宏任务后必定渲染”是常见的理解误区,但浏览器为了优化性能,可能会将多次 DOM 修改合并后统一渲染。不过,可以确定的是:微任务总是在渲染之前执行。因此,所有在微任务中完成的 DOM 变更,都会在本次渲染中一并呈现,不会出现中途刷新。
document.body.style.backgroundColor = 'red';
Promise.resolve().then(() => {
// 模拟耗时微任务,但不会导致背景色先变成红色再执行
for (let i = 0; i < 1000000; i++) {}
});
document.body.style.backgroundColor = 'blue';
页面背景色最终直接变为蓝色,中间不会闪过红色。因为两次样式变更都在同一个宏任务中,微任务即使耗时,也是在样式计算之后、绘制之前执行。
实用建议
- 拆解长任务:如果一段同步代码耗时过长,可以将其拆分为多个宏任务(用
setTimeout分开),避免阻塞 UI。 - 优先使用微任务:对于需要尽快响应的异步逻辑(如状态同步、Promise 回调),微任务比宏任务更快。
- 避免微任务过多堆积:尤其是在循环中持续触发
Promise,可能导致主线程被长期占用,此时可考虑使用setTimeout稀释。
掌握了宏任务与微任务的分类及执行顺序,你就能精准预测异步代码的执行结果,也能写出更流畅、更高效的 Web 应用。