JavaScript 的 宏任务(MacroTask) 和 微任务(MicroTask) 是事件循环机制中的核心概念,它们决定了异步代码的执行顺序。以下是详细说明:
一、核心概念
1. 宏任务(MacroTask)
- 定义:由宿主环境(浏览器或 Node.js)发起的异步任务,每次事件循环中只执行一个宏任务。
- 常见类型:
setTimeout
/setInterval
- I/O 操作(如文件读写、网络请求)
- UI 渲染(浏览器中的重绘、回流)
- 用户交互事件(如
click
、load
)- 整体脚本代码(
<script>
标签内的同步代码)requestAnimationFrame
(浏览器动画)
- 特点:
- 每次事件循环执行一个宏任务。
- 执行完毕后会处理所有微任务,再进入下一轮事件循环。
2. 微任务(MicroTask)
- 定义:高优先级的异步任务,在当前宏任务执行完毕后立即执行,且在下一个宏任务开始前清空微任务队列。
- 常见类型:
Promise.then
/catch
/finally
MutationObserver
(监听 DOM 变化)queueMicrotask
(显式添加微任务)process.nextTick
(Node.js 中优先级最高)
- 特点:
- 执行顺序优先于宏任务。
- 微任务队列会一次性清空,即使在微任务中添加新的微任务也会立即执行。
二、事件循环执行顺序
JavaScript 的事件循环遵循以下流程:
- 执行同步代码(宏任务):
- 主线程按顺序执行同步代码,遇到异步任务(如
setTimeout
、Promise
)时将其放入对应队列。
- 主线程按顺序执行同步代码,遇到异步任务(如
- 执行所有微任务:
- 当前宏任务执行完毕后,立即清空微任务队列。微任务队列中的任务会依次执行,即使在微任务中添加新的微任务也会继续执行。
- 渲染更新(浏览器环境):
- 如果需要,浏览器会进行 UI 渲染。
- 执行下一个宏任务:
- 从宏任务队列中取出下一个任务执行,重复上述流程。
三、执行顺序示例
实际上是先执行同步任务,异步任务有宏任务和微任务两种,先将宏任务添加到宏任务队列中,将宏任务里面的微任务添加到微任务队列中。所有同步执行完之后执行异步,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行。之后就一直循环…
示例 1:基础执行顺序
console.log("Start"); // 同步代码(宏任务)setTimeout(() => {console.log("setTimeout"); // 宏任务
}, 0);Promise.resolve().then(() => {console.log("Promise 1"); // 微任务
}).then(() => {console.log("Promise 2"); // 微任务
});console.log("End"); // 同步代码(宏任务)
输出结果:
Start
End
Promise 1
Promise 2
setTimeout
解析:
- 同步代码
Start
和End
优先执行。- 微任务
Promise 1
和Promise 2
在当前宏任务结束后立即执行。- 宏任务
setTimeout
在微任务队列清空后执行。
示例 2:微任务嵌套
console.log("Start"); // 同步代码Promise.resolve().then(() => {console.log("Promise 1"); // 微任务Promise.resolve().then(() => {console.log("Promise 2"); // 新增的微任务});
});console.log("End"); // 同步代码
输出结果:
Start
End
Promise 1
Promise 2
解析:
- 微任务
Promise 1
执行时,新增的Promise 2
会被立即加入微任务队列,并在当前轮事件循环中优先执行。
示例 3
setTimeout(function(){console.log('1');
});
new Promise(function(resolve){ console.log('2');resolve();
}).then(function(){ console.log('3');
}).then(function(){
console.log('4')
});
console.log('5');
// 2 5 3 4 1
解析:
1.遇到setTimout,异步宏任务,放入宏任务队列中
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(‘5’);输出5;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
6.从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空
示例 4
setTimeout(()=>{new Promise(resolve =>{resolve();}).then(()=>{console.log('test');});console.log(4);
});new Promise(resolve => {resolve();console.log(1)
}).then( () => {console.log(3);Promise.resolve().then(() => {console.log('before timeout');}).then(() => {Promise.resolve().then(() => {console.log('also before timeout')})})
})
console.log(2);
解析:
1.遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中;
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log(2),输出2;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
6.从微任务队列中取出任务a到主线程中,输出 before timeout;
7.从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
8.从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
9.从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
10.从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空
四、关键区别
特性 | 宏任务(MacroTask) | 微任务(MicroTask) |
---|---|---|
定义 | 由宿主环境发起的任务 | 由 JavaScript 自身发起的任务 |
执行顺序 | 每次事件循环执行一个宏任务 | 当前宏任务结束后立即执行所有微任务 |
常见示例 | setTimeout , setInterval , UI 渲染 | Promise.then , queueMicrotask |
优先级 | 低 | 高 |
应用场景 | 不紧急的任务(如延迟操作、批量渲染) | 高优先级任务(如 DOM 更新后的回调) |
五、实际应用与注意事项
-
控制异步代码顺序:
- 利用微任务的高优先级,确保某些代码在当前宏任务结束后立即执行。
- 示例:使用
Promise.then
替代setTimeout
控制执行顺序。
-
优化性能:
- 将轻量操作(如 DOM 更新)放在微任务中,避免阻塞宏任务。
- 将耗时操作(如复杂计算)放在宏任务中,避免 UI 卡顿。
-
避免微任务嵌套过深:
- 微任务队列清空前会执行所有新添加的微任务,可能导致宏任务被长时间阻塞。
- 示例:
Promise.then
中嵌套多个微任务时需谨慎处理。
-
setTimeout(fn, 0) 不是立即执行:
即使延迟时间为 0,setTimeout 仍需等待当前宏任务和所有微任务执行完毕。 -
微任务可能阻塞宏任务:
如果微任务队列中存在大量任务(如递归触发微任务),会导致宏任务被延迟。
六、Node.js 环境中的差异
在 Node.js 中,事件循环阶段与浏览器不同,但微任务优先级依然高于宏任务:
- 宏任务阶段:
setTimeout
/setInterval
(Timers 阶段)- I/O 回调(I/O Callbacks 阶段)
setImmediate
(Check 阶段)
- 微任务:
- 在每个阶段结束后清空微任务队列。
process.nextTick
的优先级高于其他微任务。
七、总结
-
宏任务:适合不紧急的异步操作,每次事件循环执行一个。
-
微任务:高优先级任务,在当前宏任务结束后立即执行所有微任务。
-
设计意义:通过微任务提高响应速度,通过宏任务避免阻塞主线程。