事件循环(Event Loop)是 JavaScript 处理异步操作的核心机制,确保单线程的执行模型下任务高效且不阻塞地运行。以下是其核心机制和工作流程的详细解释:
1. 核心概念
(1)单线程模型
JavaScript 是单线程的,同一时间只能执行一个任务。所有同步任务按顺序执行,异步任务(如定时器、网络请求)则通过事件循环机制调度。
(2)任务队列
异步任务的回调函数被分为两类,存放在不同的队列中:
- 宏任务队列(MacroTask Queue):包括
setTimeout、setInterval、DOM 事件回调、I/O 操作(如文件读写)等。 - 微任务队列(MicroTask Queue):包括
Promise.then、MutationObserver、process.nextTick(Node.js 特有)等。
2. 事件循环的工作流程
-
执行同步代码
所有同步任务直接进入执行栈(Call Stack)执行,遇到异步任务时,将其回调注册到对应的队列中。 -
处理微任务队列
当执行栈清空后,事件循环会依次执行微任务队列中的所有任务,直到队列为空。
注意:微任务具有高优先级,执行期间新产生的微任务会被添加到当前队列末尾,并在此次循环中执行。 -
执行一个宏任务
从宏任务队列中取出一个任务执行(如最早的setTimeout回调)。 -
重复步骤 2 和 3
循环处理微任务队列 → 执行一个宏任务 → 处理微任务队列 → ……,形成事件循环。
3. 关键特性
- 微任务优先于宏任务:每次执行栈清空后,先处理所有微任务,再执行一个宏任务。
- 渲染时机:在浏览器中,页面渲染(UI更新)通常发生在微任务队列处理完毕之后、下一个宏任务执行之前。
- 阻塞风险:长时间运行的同步代码或大量微任务会阻塞事件循环,导致页面卡顿。
4. 示例分析
console.log("Start"); // 同步任务
setTimeout(() => console.log("Timeout"), 0); // 宏任务
Promise.resolve()
.then(() => console.log("Promise 1")) // 微任务
.then(() => console.log("Promise 2")); // 微任务
console.log("End"); // 同步任务
输出顺序:
Start → End → Promise 1 → Promise 2 → Timeout
执行过程:
- 同步任务
Start、End依次执行。 - 微任务队列中的
Promise 1和Promise 2依次执行。 - 最后执行宏任务队列中的
Timeout。
5. 浏览器 vs Node.js 的事件循环
- 浏览器:以宏任务和微任务为核心,与渲染流程紧密结合。
- Node.js:使用更复杂的分阶段模型(如
timers、poll、check等阶段),process.nextTick优先级高于微任务。
6. 常见误区
setTimeout(fn, 0)并非立即执行:它只是尽快将回调加入宏任务队列,实际执行需等待当前执行栈和微任务队列清空。- 微任务嵌套可能导致死循环:
这段代码会阻塞后续任务执行,因为微任务队列永远无法清空。function loop() { Promise.resolve().then(loop); } loop();
7. 最佳实践
- 避免长时间同步任务:将复杂任务拆解为小块,使用
setTimeout或requestIdleCallback分步执行。 - 合理使用微任务:如需要高优先级更新,但注意不要过度阻塞。
- Web Workers:将计算密集型任务移至子线程,避免阻塞主线程。