JavaScript 事件循环:宏任务与微任务的执行顺序详解
JavaScript 采用单线程模型,通过事件循环(Event Loop)处理异步操作。文章核心解析了宏任务与微任务的执行优先级:在主线程同步代码执行完毕后,会先清空所有微任务队列,再执行一个宏任务。掌握这一机制对于调试 Promise、定时器及理解前端渲染逻辑至关重要。
为什么 JavaScript 需要事件循环?
JavaScript 是一门单线程语言,这意味着同一时间只能执行一段代码。这种设计主要源于其诞生使命:处理用户交互和操作 DOM。如果允许同时添加和删除 DOM 元素,将导致复杂的竞态条件。
由于单线程特性,所有任务必须排队执行。如果某个任务耗时过长(如网络请求或大量计算),页面渲染就会中断,造成卡顿。为了解决这一问题,JavaScript 引入了异步机制:
- 同步任务:直接进入主线程执行,形成执行栈。前一个任务结束,后一个任务才开始。
- 异步任务:不进入主线程,而是放入“任务队列”。只有当主线程空闲时,才会从队列中取出回调函数执行。
宏任务与微任务的本质区别
任务队列并非单一队列,而是分为宏任务(Macrotask)和微任务(Microtask)。它们的优先级决定了异步代码的执行顺序。
常见任务分类
宏任务(Macro Task)包括整体代码 script、setTimeout、setInterval、I/O 操作和 UI 交互等。每次从一个宏任务队列取一个任务执行。
微任务(Micro Task)包括 Promise.then、async/await、MutationObserver 等。只要微任务队列不为空,就会持续执行,直到清空为止。
注意:new Promise() 内部的代码是同步执行的,只有 .then() 或 .catch() 中的回调才是微任务。
事件循环的执行流程
JavaScript 引擎的执行遵循严格的顺序规则,如下图所示展示了基本的任务流转逻辑:
具体执行步骤如下:
- 执行所有同步代码(主线程)。
- 同步代码执行完毕,检查微任务队列。如果有微任务,按顺序全部执行完(包括执行过程中新增的微任务)。
- 微任务队列为空后,从宏任务队列头部取出一个宏任务执行。
- 重复上述过程:执行该宏任务中的同步代码 -> 执行产生的所有微任务 -> 取出下一个宏任务,直至队列耗尽。
实战案例演练
案例 1:基础异步逻辑
观察以下代码的输出顺序:
const promise = new Promise((resolve, reject) => {
resolve("10")
}).then(res => {
console.log("res1:", res)
return 9
}).then(res => {
console.log("res2:", res)
return 8
}).then(res => {
console.log("res3:", res)
let promise2 = new Promise((resolve, reject) => {
resolve("p2")
}).then(res => {
console.log(res)
setTimeout(function() {
console.log("setTimeout2")
}, 0)
})
})
console.log('aaa')
setTimeout(function() {
console.log("setTimeout1")
}, 0)
const promise1 = new Promise((resolve, reject) => {
console.log("p1")
resolve(989)
}).then(res => {
console.log(res)
return 990
}).then(res => {
console.log(res)
return 991
}).then(res => {
console.log(res)
return 0
})
/* 预期输出:
aaa
p1
res1: 10
989
res2: 9
990
res3: 8
991
p2
setTimeout1
setTimeout2
*/
解析:首先同步打印 aaa。接着执行宏任务脚本中的剩余微任务链。由于 promise1 定义在后但立即触发状态变更,且 promise 的 then 链也包含微任务,引擎会优先清空当前阶段的所有微任务。因此 p1、res1、989 等依次输出。最后执行剩余的宏任务 setTimeout1,并在其中产生的微任务和另一个超时任务中输出后续内容。
案例 2:结合 setTimeout 与 process.nextTick
虽然 Node.js 特有 process.nextTick,但在浏览器环境中理解其与 Promise 的关系同样重要。以下代码模拟浏览器环境下 Promise 与 setTimeout 的交替:
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
});
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
});
setTimeout(function() {
console.log('9');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
});
// 输出结果: 1 7 8 2 4 5 9 11 12
解析:
- 同步输出
1。 - 遇到两个
setTimeout,它们被加入宏任务队列,跳过执行。 - 遇到第一个
Promise,同步打印7,然后.then加入微任务队列。 - 遇到第二个
setTimeout,加入宏任务队列。 - 主线程同步部分结束,开始执行微任务,输出
8。 - 微任务清空,取出第一个宏任务
setTimeout 2。同步打印2,内部Promise打印4,.then加入微任务。 - 执行刚产生的微任务,输出
5。 - 取出第二个宏任务
setTimeout 9。同步打印9,内部Promise打印11,.then加入微任务。 - 执行微任务,输出
12。
总结
掌握 JavaScript 事件循环的关键在于记住执行顺序:“同步代码 -> 所有微任务 -> 一个宏任务 -> 所有微任务...”。在实际开发中,若需确保某段代码在 DOM 更新或异步数据返回后尽快执行,应使用微任务(如 Promise);若需让出主线程后再执行,则使用宏任务(如 setTimeout)。
以上就是 JavaScript 事件循环剖析宏任务与微任务的详细内容,更多关于 JavaScript 事件循环宏任务微任务的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









