浏览器的事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它负责协调同步任务、异步任务的执行顺序,确保单线程的 JavaScript 能够高效处理并发操作(如网络请求、定时器、用户交互等)。
一、核心背景:JavaScript 的单线程特性
JavaScript 设计为单线程(同一时间只能执行一个任务),这是因为它最初用于处理浏览器 DOM 操作 ------ 多线程可能导致 DOM 冲突(如同时修改同一个元素)。
但单线程会带来问题:如果有一个耗时任务(如网络请求),会阻塞后续代码执行,导致页面卡顿。因此,浏览器引入了事件循环机制,配合异步 API,实现 "非阻塞" 的并发效果。
二、事件循环的基本流程
事件循环的核心是协调调用栈(Call Stack) 、任务队列(Task Queue) 、微任务队列(Microtask Queue) 和浏览器内核模块的工作流程,步骤如下:
-
执行同步代码:所有同步任务直接进入调用栈,按 "先进后出" 顺序执行,执行完后出栈。
-
处理异步任务 :遇到异步任务(如
setTimeout
、fetch
、DOM事件
),JavaScript 引擎不会等待其完成,而是将其交给浏览器的内核模块(如定时器模块、网络模块、DOM 模块)处理,继续执行后续同步代码。 -
异步任务完成后入队 :当异步任务完成(如定时器到期、网络请求返回、用户点击),内核模块会将对应的回调函数 放入任务队列 (宏任务队列)或微任务队列。
-
执行队列中的任务:当调用栈为空时(同步代码执行完毕),事件循环开始工作:
- 先清空微任务队列:将所有微任务按顺序放入调用栈执行,直到微任务队列为空。
- 再执行一个宏任务:从宏任务队列取第一个任务放入调用栈执行。
- 重复以上步骤,形成循环。
三、宏任务(Macrotask)与微任务(Microtask)
异步任务分为两类,优先级不同:
1. 宏任务(Macrotask)
- 类型 :
setTimeout
、setInterval
、setImmediate
(Node 特有)、I/O操作
(如网络请求、文件读写)、DOM事件
(如 click、load)、script标签中的整体代码
。 - 特点 :每次事件循环只执行一个宏任务,执行完后会触发页面渲染(如果需要),再处理微任务。
2. 微任务(Microtask)
- 类型 :
Promise.then/catch/finally
、async/await
(本质是 Promise 语法糖)、queueMicrotask()
、MutationObserver
(监听 DOM 变化)。 - 特点 :优先级高于宏任务,一个宏任务执行完后,会清空所有微任务再执行下一个宏任务。
过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式
根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
四、经典示例:理解执行顺序
js
console.log('1'); // 同步任务
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
}).then(() => {
console.log('4'); // 微任务
});
console.log('5'); // 同步任务
执行结果 :1 → 5 → 3 → 4 → 2
解析:
- 同步代码
console.log('1')
和console.log('5')
先执行,调用栈清空。 - 处理微任务队列:执行两个
then
回调,输出3
、4
,微任务队列为空。 - 执行宏任务队列:
setTimeout
回调,输出2
。
js
function a(){
console.log(1)
Promise.resolve().then(function(){
console.log(2)
})
}
setTimeout(function(){
console.log(3)
Promise.resolve().then(a)
},0)
Promise.resolve().then(function(){
console.log(4)
})
console.log(5)
//5 4 3 1 2
js
setTimeout(() => {
console.log('1')
},0)
Promise.resolve().then(() => {
console.log('4')
})
async function asyncFun(){
console.log('3')
await Promise.resolve()
console.log('5')
}
asyncFun()
console.log('2')
// 3 2 4 5 1
五、事件循环与页面渲染的关系
每次宏任务执行完、微任务队列清空后,浏览器会检查是否需要重新渲染页面(如 DOM 更新、样式变化),然后再进入下一次事件循环。
这也是为什么频繁的 DOM 操作建议放在微任务中批量处理 ------ 减少渲染次数,提升性能。
六、总结
事件循环的核心逻辑可简化为:同步代码优先执行 → 调用栈空时,先清微任务 → 再执行一个宏任务 → 重复循环。
理解事件循环有助于解决异步代码的执行顺序问题(如回调地狱、Promise 链式调用),也是前端面试的高频考点。记住:微任务优先级高于宏任务,且同一轮循环中微任务会被全部执行。