一、事件循环的核心概念
事件循环是 JavaScript 实现异步编程的基础机制,它基于 单线程 特性,通过不断循环检查任务队列,决定何时执行异步回调。其核心逻辑可概括为:
- 主线程执行同步任务,遇到异步操作时将回调放入队列;
- 主线程空闲时(即栈为空),从队列中取出回调执行,如此循环。
二、任务队列:宏任务与微任务
事件循环中的任务分为两类,执行优先级不同:
1. 宏任务(Macrotask)
- 常见场景:
setTimeout、setInterval、script(整体代码块)、I/O、UI 渲染等。 - 队列特点:浏览器中通常有多个宏任务队列(如定时器队列、I/O队列),每次事件循环仅处理一个队列中的任务。
2. 微任务(Microtask)
- 常见场景:
Promise.then、MutationObserver、process.nextTick(Node.js)等。 - 队列特点:所有微任务在同一个队列中,优先级高于宏任务。
三、事件循环的执行流程
以浏览器环境为例,事件循环的完整流程如下:
- 初始阶段:执行主线程中的同步代码(即第一个宏任务)。
- 处理微任务:同步代码执行完毕后,立即执行微任务队列中的所有任务,直至队列为空。
- 渲染页面:微任务执行完毕后,浏览器可能会进行 UI 渲染(非必须步骤)。
- 获取下一个宏任务:从宏任务队列中取出一个任务(如定时器回调),放入主线程执行。
- 重复步骤2-4:不断循环,直至所有任务处理完毕。
四、经典示例:理解执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
Promise.resolve().then(() => {
console.log('4');
});
console.log('5');
执行结果:1 → 5 → 4 → 2 → 3
解析:
- 同步代码
console.log('1')和console.log('5')立即执行。 setTimeout是宏任务,回调进入宏任务队列。Promise.then是微任务,回调进入微任务队列。- 主线程同步代码执行完毕后,先处理微任务队列中的
console.log('4')。 - 微任务执行完毕,取出宏任务队列中的
setTimeout回调,执行console.log('2'),其内部的Promise.then再次进入微任务队列。 - 当前宏任务执行完毕,再次处理微任务队列中的
console.log('3')。
五、Node.js 与浏览器事件循环的差异
Node.js 的事件循环基于 libuv 库,流程与浏览器略有不同:
| 阶段 | 作用 | 宏任务类型(Node.js) |
|---|---|---|
timers | 处理 setTimeout/setInterval | 定时器回调 |
pending callbacks | 处理系统操作回调 | I/O 回调(如文件操作) |
idle, prepare | 内部阶段,无需关注 | - |
poll | 等待新的 I/O 事件 | 接收新的回调、处理定时器到期任务 |
check | 处理 setImmediate | setImmediate 回调 |
close callbacks | 处理关闭事件 | 如 socket.close() 回调 |
关键差异:
- Node.js 中
process.nextTick的优先级高于所有微任务(包括Promise.then)。 setImmediate是独立于定时器的宏任务,在poll阶段之后执行。
六、实战场景:避免阻塞与优化
-
避免长时间阻塞
- 大量计算任务会阻塞事件循环,导致页面卡顿,可通过
requestAnimationFrame或Web Worker拆分任务:// 错误示例:阻塞主线程 function heavyTask() { for (let i = 0; i < 1e8; i++) {} } // 优化示例:分片执行 function optimizedTask() { let count = 0; const total = 1e8; function taskSlice() { // 每次处理一小部分任务 for (let i = 0; i < 1e4 && count < total; i++) { count++; } if (count < total) { requestAnimationFrame(taskSlice); } } taskSlice(); }
- 大量计算任务会阻塞事件循环,导致页面卡顿,可通过
-
微任务与宏任务的选择
- 需立即执行的异步操作(如 Promise 链式调用)用微任务;
- 可延迟的操作(如用户交互反馈)用宏任务,避免阻塞渲染。
七、问题
-
事件循环与单线程的关系?
- 答:JavaScript 是单线程语言,事件循环通过任务队列实现异步操作,避免主线程阻塞。
-
宏任务和微任务的执行顺序?
- 答:每个宏任务执行完毕后,会立即处理所有微任务,再取下一个宏任务。
-
如何用事件循环解释异步代码顺序?
- 答:结合具体示例,分析同步代码、宏任务、微任务的入队与执行顺序(如前文示例)。
总结
事件循环机制是 JavaScript 异步编程的核心,理解其原理有助于写出更高效的代码,避免性能问题。关键要记住:微任务在宏任务之间执行,且同一轮循环中会清空所有微任务。在实际开发中,合理利用宏任务与微任务的优先级,可优化用户体验与代码执行效率。