Appearance
为什么需要事件循环?
JavaScript 是一门单线程的编程语言,这意味着它只有一个主线程来处理所有任务。如果没有事件循环机制,那么:
- 阻塞问题: 耗时操作会导致后续代码无法执行
- 资源浪费: CPU 在等待 I/O 时完全闲置
- 用户体验: 界面卡顿,无法响应用户交互
为了解决这些问题,JavaScript 引入了事件循环机制,将任务分为同步和异步两种执行模式。
二、事件循环的核心组成
1. 调用栈(Call Stack)
javascript
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
// 调用栈变化过程:
// 1. [] -> [foo]
// 2. [foo] -> [foo, bar]
// 3. [foo, bar] -> [foo]
// 4. [foo] -> []
2. 堆(Heap)
- 存储对象、数组等引用类型数据
- 进行内存分配的区域
- 由垃圾回收器自动管理
3. 任务队列(Task Queue)
分为两种类型:
宏任务(Macrotask)
javascript
// 常见宏任务
- script(整体代码)
- setTimeout/setInterval
- setImmediate(Node)
- requestAnimationFrame(浏览器)
- I/O操作
- UI rendering
- MessageChannel
微任务(Microtask)
javascript
// 常见微任务
- Promise.then/catch/finally
- process.nextTick(Node)
- MutationObserver
- queueMicrotask()
- IntersectionObserver
三、事件循环详细执行流程
执行同步代码
- 将同步代码压入调用栈
- 按顺序执行
- 清空调用栈
执行微任务
- 检查微任务队列
- 将所有微任务依次压入调用栈执行
- 清空微任务队列
执行宏任务
- 检查宏任务队列
- 取出一个宏任务执行
- 执行完后重复步骤 2
UI 渲染
- 如果需要,执行 UI 渲染
- 渲染完成后重复步骤 1
四、Promise 与事件循环
1. Promise 的执行时机
javascript
console.log('1');
new Promise((resolve) => {
console.log('2');
resolve();
}).then(() => {
console.log('3');
});
setTimeout(() => {
console.log('4');
}, 0);
console.log('5');
// 输出: 1 2 5 3 4
解析:
- Promise 构造函数是同步执行的
- .then() 回调是微任务
- setTimeout 是宏任务
2. async/await 的执行机制
javascript
async function foo() {
console.log('1');
await Promise.resolve('2');
console.log('3'); // 微任务
}
foo();
console.log('4');
// 输出: 1 4 3
await 的特点:
- await 后面的代码相当于放到 Promise.then 中执行
- 会暂停当前 async 函数的执行
- 让出执行线程
五、复杂案例分析
案例 1: 多重嵌套
javascript
console.log('script start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
new Promise((resolve) => {
console.log('promise2');
resolve();
setTimeout(() => {
console.log('timeout2');
}, 0);
}).then(() => {
console.log('promise3');
});
console.log('script end');
// 输出:
// script start
// promise2
// script end
// promise3
// timeout1
// promise1
// timeout2
案例 2: async/await 与 Promise 混合
javascript
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise3');
resolve();
}).then(() => {
console.log('promise4');
});
console.log('script end');
// 输出:
// script start
// async1 start
// promise1
// promise3
// script end
// promise2
// promise4
// async1 end
// setTimeout
六、Node.js 事件循环的特点
Node.js 的事件循环与浏览器有所不同,它分为 6 个阶段:
- timers: 执行 setTimeout/setInterval 的回调
- pending callbacks: 执行系统操作的回调
- idle, prepare: 仅系统内部使用
- poll: 获取新的 I/O 事件
- check: 执行 setImmediate 的回调
- close callbacks: 执行 close 事件的回调
javascript
// Node.js 特有的执行顺序
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
process.nextTick(() => {
console.log('nextTick');
});
// 可能的输出1:
// nextTick
// setTimeout
// setImmediate
// 可能的输出2:
// nextTick
// setImmediate
// setTimeout
七、实践建议
- 合理使用微任务
javascript
// 不推荐
setTimeout(() => {
// 处理逻辑
}, 0);
// 推荐
queueMicrotask(() => {
// 处理逻辑
});
- 注意性能问题
javascript
// 避免无限循环的微任务
function badCode() {
Promise.resolve().then(() => badCode());
}
- 代码可读性
javascript
// 推荐使用 async/await 替代 Promise 链
// 不推荐
getData()
.then(processData)
.then(saveData)
.catch(handleError);
// 推荐
async function handleData() {
try {
const data = await getData();
const processed = await processData(data);
await saveData(processed);
} catch (err) {
handleError(err);
}
}
通过深入理解事件循环机制,我们可以:
- 更好地控制代码执行顺序
- 优化应用性能
- 避免常见的异步编程陷阱
- 写出更可维护的代码
这些知识在面试中经常被考察,建议多加练习和实践。