深入 JavaScript 事件循环:单线程如何掌控异步世界

0 阅读4分钟

想象一下一家繁忙的餐厅:只有一位厨师(单线程),却能同时处理多个订单(任务)。这就是 JavaScript 事件循环的魔力。下面我将用最清晰的流程图和代码示例,揭开事件循环的核心执行机制。

核心流程图:事件循环的完整生命周期

image.png

四步详解事件循环流程

第一步:执行初始宏任务(整个 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 宏任务:关键差异

特性微任务宏任务
执行优先级⭐️⭐️⭐️⭐️⭐️(最高)⭐️⭐️⭐️(较低)
队列处理方式一次性清空全部每次循环只执行一个
典型 APIPromise.then, MutationObserversetTimeout, 事件回调
嵌套行为立即执行进入队列等待

浏览器中的完整执行流程演示

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中的微任务    ← 清空该宏任务的微任务
*/

避免事件循环的三大陷阱

  1. 微任务爆炸(阻塞渲染)

    function microtaskBomb() {
      Promise.resolve().then(microtaskBomb);
    }
    // 解决方案:拆分任务或用setTimeout
    
  2. 长任务阻塞(页面卡顿)

    // 错误示例:50ms以上的同步任务
    function longTask() {
      const start = Date.now();
      while (Date.now() - start < 100) {}
    }
    // 解决方案:拆分为小任务或用Web Worker
    
  3. 渲染时机误判

    element.style.transform = 'translateX(100px)';
    // 错误:直接读取布局信息
    const rect = element.getBoundingClientRect(); 
    
    // 正确:在微任务中获取
    Promise.resolve().then(() => {
      const rect = element.getBoundingClientRect();
    });
    

浏览器 vs Node.js 事件循环差异

特性浏览器Node.js
微任务执行时机宏任务结束后事件阶段切换时
process.nextTick不支持优先级高于微任务
渲染机制专用渲染阶段

掌握事件循环的四大价值

  1. 性能优化:合理拆分任务,保持页面流畅(FPS > 60)
  2. 精准控制:确保 DOM 操作在正确时机执行
  3. 异步编程:深入理解 Promise/async/await 执行顺序
  4. 面试必过: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 的异步精髓!