什么是浏览器的事件循环 (Event Loop)?
浏览器的事件循环 (Event Loop) 是 JavaScript 运行时环境(Runtime)处理同步和异步任务的核心机制。
简单来说,它是 JavaScript 实现“单线程非阻塞”的关键。它负责协调代码执行、事件处理(如点击)、定时器执行和网络请求回调等。
以下是通俗易懂的详细解释:
1. 为什么需要事件循环?
JavaScript 是单线程的。这意味着它同一时间只能做一件事。
如果 JS 只有同步执行(一行一行往下走),那么当遇到一个耗时的操作(比如从服务器获取数据)时,整个页面就会“卡死”,用户无法点击任何东西,直到数据回来。
为了解决这个问题,浏览器采用了异步机制。Event Loop 就是负责管理这些异步任务何时进入主线程执行的“调度员”。
2. 核心概念与组件
要理解 Event Loop,需要知道以下几个区域:
调用栈 (Call Stack):
- 这是主线程,负责执行所有的同步代码。
- 代码执行时遵循“后进先出”原则。
- 只有当调用栈为空时,Event Loop 才会去搬运异步任务进来。
Web APIs (浏览器提供的能力):
- 当 JS 遇到
setTimeout、fetch、DOM事件时,会把这些任务交给浏览器的 Web APIs 去处理(比如浏览器有专门的计时器线程、网络线程)。 - 当这些任务完成(时间到了、数据回来了),它们会将回调函数放入任务队列。
- 当 JS 遇到
任务队列 (Task Queues):
- 这是异步任务排队等待进入调用栈的地方。
- 队列分为两种:宏任务队列 和 微任务队列。
3. 宏任务 (Macro-task) vs 微任务 (Micro-task)
这是面试中最常考的细节,两者的优先级不同。
宏任务 (Macro-task):
- 代表性的任务:
script(整体代码)、setTimeout、setInterval、setImmediate(Node.js)、I/O、UI 渲染。 - 特点:每次 Event Loop 循环只取一个宏任务执行。
- 代表性的任务:
微任务 (Micro-task):
- 代表性的任务:
Promise.then/catch/finally、MutationObserver、queueMicrotask。 - 特点:优先级高。在当前宏任务执行完后,所有排队的微任务会一次性全部执行完,才会进行下一次循环。
- 代表性的任务:
4. Event Loop 的执行流程 (循环步骤)
浏览器的 Event Loop 遵循以下严格步骤:
- 执行栈选择:从宏任务队列中取出一个任务(刚开始是整个 script 代码)放入调用栈执行。
- 执行同步代码:执行过程中,如果遇到微任务(如 Promise),将其放入微任务队列;如果遇到宏任务(如 setTimeout),交给 Web API 处理,完成后放入宏任务队列。
- 清空微任务队列:当调用栈为空(当前宏任务执行完毕)时,立即查看微任务队列。如果有任务,就一个接一个地执行,直到微任务队列清空。
- UI 渲染:浏览器判断是否需要更新页面(通常是 60Hz,即每 16.6ms 一次)。如果需要,就在这里渲染。
- 下一轮循环:回到第 1 步,从宏任务队列中取出下一个任务执行。
简记口诀:
同步代码 -> 清空所有微任务 -> (尝试渲染) -> 取出一个宏任务 -> 循环
5. 实战代码示例
请看下面的代码,试着预测输出顺序:
javascript
console.log('1'); // 同步
setTimeout(function() {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(function() {
console.log('3'); // 微任务
});
console.log('4'); // 同步
执行分析:
第一轮 (Script 宏任务):
- 遇到
console.log('1')-> 输出 1。 - 遇到
setTimeout-> 它是宏任务,交给 Web API 计时(0ms),回调函数进入宏任务队列等待。 - 遇到
Promise.then-> 它是微任务,回调函数进入微任务队列等待。 - 遇到
console.log('4')-> 输出 4。 - Script 代码执行完毕,调用栈清空。
- 遇到
检查微任务:
- 发现微任务队列里有
console.log('3')。 - 执行它 -> 输出 3。
- 微任务队列清空。
- 发现微任务队列里有
开启下一轮 (新的宏任务):
- 检查宏任务队列,发现
setTimeout的回调。 - 执行它 -> 输出 2。
- 检查宏任务队列,发现
最终结果: 1 -> 4 -> 3 -> 2
6. 进阶示例 (嵌套情况)
javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
});
console.log('6');
结果: 1 -> 6 -> 4 -> 2 -> 3 -> 5
分析:
- 同步:输出
1,输出6。- 宏队列:[
setTimeout(log2)] - 微队列:[
Promise(log4)]
- 宏队列:[
- 清空微任务:执行
Promise(log4)-> 输出 4。- 执行过程中遇到了
setTimeout(log5),将其加入宏队列。 - 宏队列现在是:[
setTimeout(log2),setTimeout(log5)]
- 执行过程中遇到了
- 第一轮结束。
- 下一轮宏任务:取出
setTimeout(log2)执行 -> 输出 2。- 执行中遇到
Promise(log3),加入微队列。
- 执行中遇到
- 清空微任务:执行
Promise(log3)-> 输出 3。 - 下一轮宏任务:取出
setTimeout(log5)执行 -> 输出 5。
总结
- JS 是单线程的,依靠 Event Loop 处理异步。
- 同步代码最先执行。
- 微任务 (Promise) 的优先级高于 宏任务 (setTimeout)。
- 微任务会在当前宏任务结束、下一次渲染之前全部执行完。