JavaScript 事件循环机制详解
1. 事件循环基本概念
1.1 什么是事件循环?
事件循环是 JavaScript 处理异步任务的核心机制,它让单线程的 JavaScript 能够"同时"处理多个任务。
javascript
console.log('1. 开始');
setTimeout(() => {
console.log('2. 定时器');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
console.log('4. 结束');
// 执行顺序:1 → 4 → 3 → 2
// 这就是事件循环在起作用!
1.2 为什么需要事件循环?
JavaScript 是单线程的,如果没有事件循环,遇到耗时任务时页面就会卡死。
javascript
// 如果没有事件循环 - 糟糕的情况
console.log('开始');
const result = synchronousNetworkRequest(); // 假设这个请求需要3秒
console.log('收到结果:', result);
console.log('页面卡住了3秒!用户无法操作!');
// 有事件循环 - 良好的体验
console.log('开始');
asynchronousNetworkRequest((result) => {
console.log('收到结果:', result);
});
console.log('页面可以立即响应!用户可以正常操作!');
2. 事件循环的组成部分
2.1 三大核心组件
javascript
// 可视化事件循环结构
┌───────────────────────────┐
│ 调用栈 (Call Stack) │ ← 正在执行的代码
└───────────────────────────┘
↓
┌───────────────────────────┐
│ Web APIs 环境 │ ← 浏览器提供的异步API
│ - setTimeout │
│ - DOM事件 │
│ - 网络请求 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 任务队列 (Task Queue) │ ← 等待执行的回调函数
│ 1. 宏任务队列 │
│ 2. 微任务队列 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 事件循环 (Event Loop) │ ← 协调调度的"管理员"
└───────────────────────────┘
2.2 调用栈 (Call Stack)
javascript
function first() {
console.log('第一个函数开始');
second();
console.log('第一个函数结束');
}
function second() {
console.log('第二个函数开始');
third();
console.log('第二个函数结束');
}
function third() {
console.log('第三个函数');
}
first();
// 调用栈变化:
// 1. first() 入栈
// 2. console.log() 入栈 → 执行 → 出栈
// 3. second() 入栈
// 4. console.log() 入栈 → 执行 → 出栈
// 5. third() 入栈
// 6. console.log() 入栈 → 执行 → 出栈
// 7. third() 出栈
// 8. console.log() 入栈 → 执行 → 出栈
// 9. second() 出栈
// 10. first() 出栈
3. 任务队列详解
3.1 微任务 (Microtasks) vs 宏任务 (Macrotasks)
javascript
console.log('脚本开始'); // 同步任务
// 宏任务
setTimeout(() => {
console.log('setTimeout - 宏任务');
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log('Promise - 微任务');
});
// 另一个微任务
queueMicrotask(() => {
console.log('queueMicrotask - 微任务');
});
console.log('脚本结束'); // 同步任务
// 执行顺序:
// 1. 脚本开始 (同步)
// 2. 脚本结束 (同步)
// 3. Promise - 微任务
// 4. queueMicrotask - 微任务
// 5. setTimeout - 宏任务
3.2 微任务有哪些?
javascript
// 常见的微任务来源:
Promise.then() / .catch() / .finally()
queueMicrotask()
MutationObserver(DOM变化观察)
process.nextTick(Node.js)
// 微任务特点:优先级高,在每个宏任务之后立即执行
3.3 宏任务有哪些?
javascript
// 常见的宏任务来源:
setTimeout / setInterval
setImmediate(Node.js)
I/O 操作(文件读取、网络请求)
UI 渲染(浏览器)
DOM 事件(click、load等)
// 宏任务特点:优先级较低,等待调用栈清空后执行
4. 完整的事件循环流程
4.1 详细执行步骤
javascript
// 步骤演示
console.log('1. 同步任务开始');
// 宏任务
setTimeout(() => {
console.log('6. 宏任务 - setTimeout');
Promise.resolve().then(() => {
console.log('7. 微任务 - 在宏任务中');
});
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log('4. 微任务 - Promise 1');
});
Promise.resolve().then(() => {
console.log('5. 微任务 - Promise 2');
});
console.log('2. 同步任务继续');
setTimeout(() => {
console.log('8. 另一个宏任务');
}, 0);
console.log('3. 同步任务结束');
// 执行过程分析:
4.2 事件循环算法
1. 执行同步代码(调用栈)
2. 调用栈清空后,检查微任务队列
3. 执行所有微任务(直到微任务队列清空)
4. 必要时进行UI渲染
5. 从宏任务队列取一个任务执行
6. 回到步骤2,循环...
5. 实际代码演示
5.1 复杂示例分析
javascript
console.log('start'); // 1. 同步
setTimeout(function() {
console.log('timeout1'); // 5. 宏任务
Promise.resolve().then(function() {
console.log('promise1'); // 6. 微任务
});
}, 0);
Promise.resolve().then(function() {
console.log('promise2'); // 3. 微任务
setTimeout(function() {
console.log('timeout2'); // 7. 宏任务
}, 0);
});
console.log('end'); // 2. 同步
// 输出顺序:
// start
// end
// promise2
// timeout1
// promise1
// timeout2
5.2 嵌套任务执行顺序
javascript
console.log('1. 开始');
setTimeout(() => {
console.log('2. 外层宏任务');
Promise.resolve().then(() => {
console.log('3. 外层微任务');
});
setTimeout(() => {
console.log('4. 内层宏任务');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('5. 外层微任务');
setTimeout(() => {
console.log('6. 微任务中的宏任务');
}, 0);
});
console.log('7. 结束');
// 执行顺序分析:
// 1 → 7 → 5 → 2 → 3 → 6 → 4
6. 浏览器 vs Node.js 事件循环差异
6.1 浏览器事件循环
javascript
// 浏览器中的阶段:
// 1. 执行同步代码
// 2. 执行微任务
// 3. UI渲染(如果需要)
// 4. 执行宏任务
console.log('脚本开始');
// 宏任务
setTimeout(() => console.log('计时器'));
// 微任务
Promise.resolve().then(() => console.log('Promise'));
// 动画帧回调(在渲染前执行)
requestAnimationFrame(() => console.log('RAF'));
console.log('脚本结束');
6.2 Node.js 事件循环
javascript
// Node.js 有更复杂的阶段:
// timers → pending callbacks → idle, prepare → poll → check → close callbacks
console.log('开始');
setTimeout(() => console.log('timer1'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('结束');
// Node.js 输出可能:
// 开始 → 结束 → nextTick → promise → timer1 → immediate
// 或者:开始 → 结束 → nextTick → promise → immediate → timer1
7. 常见面试题分析
7.1 经典面试题
javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
});
console.log('6');
// 输出顺序:1 → 4 → 6 → 5 → 2 → 3
7.2 进阶面试题
javascript
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 输出顺序:
// script start → async1 start → async2 → promise1 → script end
// → async1 end → promise2 → setTimeout
8. 性能优化实践
8.1 避免阻塞事件循环
javascript
// 错误示范:同步耗时操作阻塞事件循环
function processLargeData(data) {
// 这个函数执行时间很长,会阻塞页面
for (let i = 0; i < 1000000; i++) {
// 繁重的同步计算
}
}
// 正确示范:将任务分解为异步执行
async function processLargeDataAsync(data) {
for (let i = 0; i < data.length; i += 1000) {
const chunk = data.slice(i, i + 1000);
// 使用 setTimeout 或 Promise 让出控制权
await new Promise(resolve => setTimeout(resolve, 0));
processChunk(chunk);
}
}
8.2 合理使用微任务和宏任务
javascript
// 紧急任务使用微任务
function urgentTask() {
Promise.resolve().then(() => {
// 需要立即执行的任务
updateUI();
});
}
// 非紧急任务使用宏任务
function nonUrgentTask() {
setTimeout(() => {
// 可以延迟执行的任务
logAnalytics();
}, 0);
}
9. 调试技巧
9.1 查看任务队列
javascript
// 添加调试信息
let microtaskCount = 0;
let macrotaskCount = 0;
// 包装 Promise 来跟踪微任务
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
microtaskCount++;
console.log(`微任务创建,总数: ${microtaskCount}`);
return originalThen.apply(this, args);
};
// 跟踪宏任务
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(...args) {
macrotaskCount++;
console.log(`宏任务创建,总数: ${macrotaskCount}`);
return originalSetTimeout.apply(this, args);
};
10. 总结
事件循环要点:
- 同步代码优先:先执行完所有同步任务
- 微任务优先:微任务在宏任务之前执行
- 队列清空:每个宏任务执行后都会清空微任务队列
- 循环不断:事件循环持续检查新任务
记忆口诀:
"同微宏,微先走,宏之后,微清空"