本文深入解析JavaScript单线程模型下的事件循环机制,详细对比浏览器与Node.js环境中宏任务与微任务的执行差异。通过剖析HTML5规范与libuv架构,阐明Main Script、微任务队列及宏任务各阶段的执行优先级,并结合经典面试题拆解Promise、setTimeout等异步操作的输出顺序,帮助开发者掌握异步编程核心原理。
前言
JavaScript代码在执行过程中,主要存在两种运行环境:浏览器环境和Node.js环境。
这两种环境下的异步处理机制虽有共通之处,但底层实现存在显著差异。
由于JavaScript引擎采用单线程模型,若同步执行耗时操作(如网络请求或长时间计算),将阻塞后续代码的执行,导致性能下降。
为了解决单线程阻塞问题,浏览器和Node.js环境均引入了事件循环(Event Loop)机制来处理异步任务。
浏览器的事件循环
浏览器的事件循环
当JavaScript线程遇到耗时操作时,会将这些任务委托给浏览器其他模块处理。这些任务根据类型被放入不同的队列:宏任务队列(Macrotask Queue)或微任务队列(Microtask Queue)。只有当微任务队列为空时,才会从宏任务队列中取出任务交由JavaScript线程执行。
事件循环的大致流程如图所示:

浏览器的宏任务、微任务
当浏览器接收到无法同步处理的事件时,会根据事件类型将其分配至宏任务或微任务队列。区分两者的关键在于任务的性质。
通常情况下,不同事件被归类如下:
宏任务队列(macrotask queue):包括Ajax请求、setTimeout、setInterval、DOM监听、UI渲染等。
微任务队列(microtask queue):包括Promise的then/catch/finally回调、Mutation Observer API以及queueMicrotask()。
这些任务的执行顺序遵循特定规则。
核心原则是:宏任务的执行必须确保微任务队列为空,即微任务队列中的所有事件全部执行完毕后,才会执行宏任务。
其次,主脚本(Main Script)中的同步代码拥有最高优先级,最先执行。
因此,整体执行顺序为:主脚本代码 > 微任务队列事件 > 宏任务事件。
面试题一
题目如下:
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
queueMicrotask(() => {
console.log("queueMicrotask1")
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2分析如下:
在第一个事件循环周期中,主脚本、宏任务和微任务包含的事件如下:
在判断任务加入宏任务还是微任务队列时,遵循以下原则:
宏任务队列(macrotask queue):Ajax、setTimeout、setInterval、DOM监听、UI Rendering等 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()。
根据上述原则,第一轮事件循环中的任务分布如下:

执行顺序为:先执行主脚本,接着执行微任务队列中的任务,最后执行宏任务队列中的任务。
// promise1 // 2 // then1 // queueMicrotask1 // then3
随后执行setTimeout1的宏任务,此时进入第二轮事件循环,内容如下:

第二轮事件循环执行内容如下:
// setTimeout1 // then2 // then4 // setTimeout2
综上所述,最终的执行结果为:
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2面试题二
题目如下:
// async function bar() {
// console.log("22222")
// return new Promise((resolve) => {
// resolve()
// })
// }
// async function foo() {
// console.log("111111")
// await bar()
// console.log("33333")
// }
// foo()
// console.log("444444")
async function async1 () {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise (function (resolve) {
console.log('promise1')
resolve();
}).then (function () {
console.log('promise2')
})
console.log('script end')
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout第一轮事件循环中的任务如下:

按照指定顺序执行后,最终结果如下:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeoutnode的事件循环
node的事件循环
浏览器的事件循环基于HTML5规范实现,不同浏览器可能存在实现差异;而Node.js中的事件循环由libuv库实现。
首先观察Node.js的架构图:
从图中可以看出,事件循环由libuv实现,libuv主要维护一个事件循环(Event Loop)和一个线程池(worker threads)。
libuv是一个跨平台的异步IO库,最初为Node.js开发,现也应用于Luvit、Julia、pyuv等项目。
EventLoop负责调用系统其他操作,如文件IO、网络通信、子进程管理等。

如图所示,事件循环充当桥梁,连接应用程序的JavaScript代码(左侧)与系统调用及线程池(右侧)。
无论是文件IO、数据库操作、网络IO、定时器还是子进程,在完成操作后,会将结果和回调函数放入事件循环(任务队列)中。
事件循环会不断从任务队列中取出事件(回调函数)并执行。
一次完整的事件循环Tick分为多个阶段:
- 定时器(Timers):执行已被setTimeout()和setInterval()调度的回调函数。
- 待定回调(Pending Callback):执行某些系统操作(如TCP错误类型)的回调,例如TCP连接时接收到ECONNREFUSED。idle, prepare:仅系统内部使用。
- 轮询(Poll):检索新的I/O事件;执行与I/O相关的回调。
- 检测(check):在此阶段执行setImmediate()回调函数。
- 关闭的回调函数:执行如socket.on('close', ...)等关闭相关的回调。

node的宏任务、微任务
Node.js中同样存在微任务和宏任务,执行原则与浏览器一致:先执行微任务,再执行宏任务。但对于宏任务,需按照上述阶段从上到下的顺序执行。
具体常见事件的执行顺序如下:
在微任务队列中:
- next tick queue:process.nextTick。
- other queue:Promise的then回调、queueMicrotask。
(按照从上往下的事件顺序执行)
在宏任务队列中:
- timer queue:setTimeout、setInterval。
- poll queue:IO事件。
- check queue:setImmediate。
- close queue:close事件。
(同样是按照从上往下的事件顺序执行)
综上所述,在每一次事件循环的Tick中,代码执行顺序如下:
next tick microtask queue; other microtask queue; timer queue; poll queue; check queue; close queue
当然,主脚本(Main Script)依然最先执行,只有主脚本执行结束后,才会按照上述顺序执行代码。
面试题一
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nexttick1
// nexttick2
// async1 end
// promise3
// settimetout0
// setImmediate
// setTimeout2第一轮事件循环中的任务如下:

按照顺序自左向右执行,3秒后执行setTimeout2。
最后的结果是:
// script start // async1 start // async2 // promise1 // promise2 // script end // nexttick1 // nexttick2 // async1 end // promise3 // settimetout0 // setImmediate // setTimeout2
到此这篇关于JS事件循环-微任务-宏任务(原理讲解+面试题分析)的文章就介绍到这了,更多相关循环-微任务-宏任务内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!