JavaScript 的事件循环(Event Loop)是其实现异步编程的核心机制,它通过任务队列管理代码的执行顺序。由于 JavaScript 是单线程的,事件循环确保了异步任务(如定时器、网络请求、Promise 等)能够在不阻塞主线程的情况下高效执行。事件循环的运作依赖于宏任务(Macro Task)和微任务(Micro Task)的协作。下面通过概念解释和代码实例详细说明。
一、事件循环的核心概念
1. 任务队列的分类
-
宏任务(Macro Task):
setTimeout
、setInterval
- I/O 操作(如文件读写、网络请求)
- DOM 事件回调(如点击事件)
script
标签中的同步代码(整体作为一个宏任务)setImmediate
(Node.js 独有)
-
微任务(Micro Task):
Promise
的then
/catch
/finally
回调MutationObserver
(浏览器环境)process.nextTick
(Node.js 环境,优先级最高)
2. 事件循环的执行流程
- 执行同步代码:主线程按顺序执行同步代码。
- 清空微任务队列:同步代码执行完毕后,依次执行所有微任务。
- 执行一个宏任务:从宏任务队列中取出第一个任务执行。
- 重复循环:重复步骤 2 和 3,直到所有队列为空。
关键规则:
- 微任务优先级高于宏任务:每次执行完一个宏任务后,必须清空微任务队列。
- 微任务会“插队”:如果在执行微任务时又产生了新的微任务,这些新微任务会在此次循环中被执行。
二、代码实例分析
示例 1:基础顺序
console.log("Start"); // 同步代码
setTimeout(() => {
console.log("Timeout"); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log("Promise"); // 微任务
});
console.log("End"); // 同步代码
执行顺序:
- 同步代码:输出
Start
→End
。 - 微任务队列:执行
Promise
回调,输出Promise
。 - 宏任务队列:执行
setTimeout
回调,输出Timeout
。
输出结果:
Start
End
Promise
Timeout
示例 2:嵌套微任务与宏任务
console.log("Start");
// 宏任务 1
setTimeout(() => {
console.log("Timeout 1");
// 宏任务 1 内部产生微任务
Promise.resolve().then(() => {
console.log("Promise 1");
});
}, 0);
// 宏任务 2
setTimeout(() => {
console.log("Timeout 2");
}, 0);
// 微任务 1
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End");
执行顺序:
- 同步代码:输出
Start
→End
。 - 微任务队列:执行
Promise 2
,输出Promise 2
。 - 宏任务队列:
- 取出第一个宏任务(
Timeout 1
)执行,输出Timeout 1
。 - 执行该宏任务内部的微任务(
Promise 1
),输出Promise 1
。 - 取出下一个宏任务(
Timeout 2
)执行,输出Timeout 2
。
- 取出第一个宏任务(
输出结果:
Start
End
Promise 2
Timeout 1
Promise 1
Timeout 2
示例 3:微任务在宏任务中触发
console.log("Start");
// 宏任务 1
setTimeout(() => {
console.log("Timeout 1");
// 宏任务 1 内部触发微任务
Promise.resolve().then(() => {
console.log("Promise 1");
});
}, 0);
// 微任务 1
Promise.resolve().then(() => {
console.log("Promise 2");
// 微任务 1 内部触发宏任务
setTimeout(() => {
console.log("Timeout 2");
}, 0);
});
console.log("End");
执行顺序:
- 同步代码:输出
Start
→End
。 - 微任务队列:执行
Promise 2
,输出Promise 2
,并将Timeout 2
加入宏任务队列。 - 宏任务队列:
- 取出第一个宏任务(
Timeout 1
)执行,输出Timeout 1
。 - 执行该宏任务内部的微任务(
Promise 1
),输出Promise 1
。 - 取出下一个宏任务(
Timeout 2
)执行,输出Timeout 2
。
- 取出第一个宏任务(
输出结果:
Start
End
Promise 2
Timeout 1
Promise 1
Timeout 2
三、关键总结
- 同步代码优先执行:所有同步代码是事件循环的起点。
- 微任务队列必须清空:每次执行完一个宏任务后,会立即清空所有微任务。
- 宏任务按队列顺序执行:每次事件循环只执行一个宏任务,避免长时间阻塞。
- 微任务可“无限嵌套”:如果在微任务中又产生了新的微任务,这些新任务会在当前循环中被执行。
四、实际应用中的注意事项
-
避免阻塞事件循环:
- 避免在同步代码中执行耗时操作(如大循环),否则会阻塞后续任务。
- 使用
Web Worker
处理 CPU 密集型任务。
-
合理使用微任务:
- 微任务适合处理高优先级的逻辑(如 Promise 链式调用)。
- 避免在微任务中嵌套过多任务,导致页面渲染延迟。
-
理解浏览器与 Node.js 的差异:
- 浏览器中,微任务在渲染前执行;Node.js 中
process.nextTick
优先级最高。 - Node.js 的
setImmediate
和setTimeout
的触发顺序可能不同。
- 浏览器中,微任务在渲染前执行;Node.js 中
通过理解事件循环的机制,可以编写出高效、可预测的异步代码,避免因执行顺序问题导致的 Bug。