想象一下一家繁忙的餐厅:只有一位厨师(单线程),却能同时处理多个订单(任务)。这就是 JavaScript 事件循环的魔力。下面我将用最清晰的流程图和代码示例,揭开事件循环的核心执行机制。
核心流程图:事件循环的完整生命周期
四步详解事件循环流程
第一步:执行初始宏任务(整个 Script)
- 整个
<script>
标签被视为第一个宏任务 - 立即执行其中的同步代码
- 遇到异步 API 时,将其回调注册到对应队列
console.log('脚本开始'); // 同步任务 → 立即执行
setTimeout(() => {
console.log('setTimeout回调'); // → 宏任务队列
}, 0);
Promise.resolve().then(() => {
console.log('Promise微任务'); // → 微任务队列
});
console.log('脚本结束'); // 同步任务 → 立即执行
第二步:清空微任务队列(最高优先级)
- 同步任务执行完毕后,立即处理微任务队列
- 必须一次性清空所有微任务(包括嵌套微任务)
- 此阶段是获取最新 DOM 状态的最佳时机
// 微任务嵌套示例
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => {
console.log('嵌套微任务'); // 会在此阶段一并执行
});
});
// 执行顺序:微任务1 → 嵌套微任务
第三步:页面渲染(浏览器环境)
- 执行 DOM 更新、样式计算、布局和绘制
- 关键点:此时用户能看到页面更新
// 获取渲染前的最新布局信息
const observer = new MutationObserver(() => {
console.log('DOM已更新,尺寸:', element.getBoundingClientRect());
});
observer.observe(element, { attributes: true });
第四步:执行下一个宏任务
- 从宏任务队列中取出一个任务执行
- 重复整个流程:同步任务 → 微任务 → 渲染
setTimeout(() => {
console.log('宏任务1开始');
Promise.resolve().then(() => {
console.log('宏任务中的微任务');
});
console.log('宏任务1结束');
}, 0);
// 执行顺序:
// 宏任务1开始 → 宏任务1结束 → 宏任务中的微任务
微任务 vs 宏任务:关键差异
特性 | 微任务 | 宏任务 |
---|---|---|
执行优先级 | ⭐️⭐️⭐️⭐️⭐️(最高) | ⭐️⭐️⭐️(较低) |
队列处理方式 | 一次性清空全部 | 每次循环只执行一个 |
典型 API | Promise.then , MutationObserver | setTimeout , 事件回调 |
嵌套行为 | 立即执行 | 进入队列等待 |
浏览器中的完整执行流程演示
console.log('同步任务1');
setTimeout(() => {
console.log('宏任务1');
Promise.resolve().then(() => console.log('宏1中的微任务'));
}, 0);
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => console.log('嵌套微任务'));
});
console.log('同步任务2');
/* 执行顺序解析:
1. 同步任务1
2. 同步任务2
3. 微任务1 ← 清空微任务队列
4. 嵌套微任务 ← 清空嵌套微任务
5. [页面渲染] ← 渲染时机
6. 宏任务1 ← 执行下一个宏任务
7. 宏1中的微任务 ← 清空该宏任务的微任务
*/
避免事件循环的三大陷阱
-
微任务爆炸(阻塞渲染)
function microtaskBomb() { Promise.resolve().then(microtaskBomb); } // 解决方案:拆分任务或用setTimeout
-
长任务阻塞(页面卡顿)
// 错误示例:50ms以上的同步任务 function longTask() { const start = Date.now(); while (Date.now() - start < 100) {} } // 解决方案:拆分为小任务或用Web Worker
-
渲染时机误判
element.style.transform = 'translateX(100px)'; // 错误:直接读取布局信息 const rect = element.getBoundingClientRect(); // 正确:在微任务中获取 Promise.resolve().then(() => { const rect = element.getBoundingClientRect(); });
浏览器 vs Node.js 事件循环差异
特性 | 浏览器 | Node.js |
---|---|---|
微任务执行时机 | 宏任务结束后 | 事件阶段切换时 |
process.nextTick | 不支持 | 优先级高于微任务 |
渲染机制 | 专用渲染阶段 | 无 |
掌握事件循环的四大价值
- 性能优化:合理拆分任务,保持页面流畅(FPS > 60)
- 精准控制:确保 DOM 操作在正确时机执行
- 异步编程:深入理解 Promise/async/await 执行顺序
- 面试必过:90% 前端面试考察事件循环相关题目
终极挑战:分析以下代码输出顺序
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
/*
答案:
1 → 6 → 4 → 2 → 3 → 5
解析:
1. 同步:1、6
2. 微任务:4(注册宏任务5)
3. 宏任务:2(执行中产生微任务3)
4. 微任务:3
5. 宏任务:5
*/
理解事件循环就像掌握了 JavaScript 引擎的 DNA。当你下次看到异步代码时,脑海中能自动浮现这个执行流程,就真正掌握了 JavaScript 的异步精髓!