JavaScript 事件循环 (Event Loop) 原理
核心概念
事件循环是JavaScript实现异步编程的关键机制,让单线程的JavaScript能够处理非阻塞I/O操作。
基本架构
调用栈 (Call Stack) ← 微任务队列 (Microtask Queue)
↓ ↑
消息队列 (Task Queue) ↑
↓ ↑
事件循环 (Event Loop) ——→
主要组成部分
1. 调用栈 (Call Stack)
- 后进先出(LIFO)结构
- 存储执行上下文
- 同步代码执行的位置
2. 堆 (Heap)
- 内存分配区域
- 存储对象、函数等引用类型
3. 队列 (Queue)
任务队列 (Task Queue / MacroTask Queue)
- 宏任务:setTimeout、setInterval、I/O、UI渲染、事件回调
- 一次事件循环处理一个宏任务
微任务队列 (Microtask Queue)
- 微任务:Promise.then/catch/finally、MutationObserver、queueMicrotask
- 优先级高于宏任务
事件循环执行顺序
- 执行同步代码(调用栈)
- 清空微任务队列
- 执行一个宏任务
- 再次清空微任务队列
- UI渲染(如果需要)
- 重复步骤3-5
代码示例
console.log('1. 同步代码开始'); // 同步任务
setTimeout(() => {
console.log('4. 宏任务执行');
}, 0);
Promise.resolve().then(() => {
console.log('3. 微任务执行');
});
console.log('2. 同步代码结束');
// 执行顺序:
// 1. 同步代码开始
// 2. 同步代码结束
// 3. 微任务执行
// 4. 宏任务执行
浏览器 vs Node.js
浏览器环境
- 基于HTML规范
- 每个浏览器标签页独立的事件循环
- 宏任务:script代码块、setTimeout、事件回调、I/O
Node.js环境
-
基于libuv库
-
更复杂的事件循环阶段:
timers → pending callbacks → idle, prepare → poll → check → close callbacks -
包含process.nextTick队列(优先级最高)
实战注意事项
- 避免长时间同步任务:会阻塞事件循环
- 微任务递归:可能导致无限递归
- 合理使用异步:I/O密集型操作用异步API
- Web Workers:CPU密集型任务可使用Worker
性能优化建议
- 将耗时操作分解为小任务
- 使用requestAnimationFrame优化动画
- 合理使用防抖和节流
- 避免在微任务中执行耗时操作
常见面试题
// 经典面试题
setTimeout(() => console.log(1), 0);
Promise.resolve().then(() => console.log(2));
console.log(3);
// 输出:3 2 1
理解事件循环原理对于编写高效、无阻塞的JavaScript代码至关重要,特别是在处理异步操作、动画和用户交互时。