返回首页

15. 说说你对事件循环Event Loop的理解?

问题解析

JavaScript是单线程语言,事件循环(Event Loop)是实现异步编程的核心机制。理解事件循环的工作原理对于理解JavaScript的异步执行顺序、性能优化以及调试都至关重要。

核心概念

为什么需要事件循环

JavaScript是一门单线程的语言,意味着同一时间内只能做一件事。但是这并不意味着单线程就是阻塞的,而实现单线程非阻塞的方法就是事件循环。

任务分类

在JavaScript中,所有的任务都可以分为:

  • 同步任务:立即执行的任务,一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求、setTimeout定时函数等

详细解答

事件循环的基本流程

┌─────────────────────────┐
│        主线程            │
│    (同步任务执行栈)       │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│      任务队列            │
│  ┌─────────────────┐    │
│  │    微任务队列    │    │
│  │  Promise.then    │    │
│  │  MutationObserver│    │
│  │  process.nextTick│    │
│  └─────────────────┘    │
│  ┌─────────────────┐    │
│  │    宏任务队列    │    │
│  │  setTimeout      │    │
│  │  setInterval     │    │
│  │  I/O操作         │    │
│  └─────────────────┘    │
└─────────────────────────┘

执行机制

  1. 同步任务进入主线程执行
  2. 异步任务进入任务队列等待
  3. 主线程任务执行完毕,检查微任务队列
  4. 执行所有微任务
  5. 执行一个宏任务
  6. 重复步骤3-5
console.log(1);

setTimeout(() => {
  console.log(2);
}, 0);

Promise.resolve().then(() => {
  console.log(3);
});

console.log(4);

// 输出: 1 -> 4 -> 3 -> 2

深入理解

宏任务(MacroTask)

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的。

常见的宏任务

  • script(外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

微任务(MicroTask)

微任务是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

常见的微任务

  • Promise.then/catch/finally
  • MutationObserver
  • Object.observe(已废弃)
  • process.nextTick(Node.js)

执行顺序示例

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. console.log(1) - 同步,输出1
  2. setTimeout - 宏任务,放入宏任务队列
  3. Promise.then - 微任务,放入微任务队列
  4. console.log(6) - 同步,输出6
  5. 同步代码执行完毕,执行微任务,输出4,同时新的setTimeout放入宏任务队列
  6. 执行宏任务(按放入顺序),先执行第一个setTimeout,输出2,遇到Promise.then放入微任务
  7. 执行微任务,输出3
  8. 执行下一个宏任务,输出5

复杂场景分析

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

async1();

new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});

console.log('script end');

输出顺序:script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout

最佳实践

  1. 避免长时间运行的同步任务:会阻塞事件循环
  2. 合理使用微任务:微任务过多会阻塞渲染
  3. 分解大任务:使用setTimeout将大任务分解
// 优化前:长时间运行的任务阻塞UI
function processLargeArray(data) {
  for (let i = 0; i < data.length; i++) {
    processItem(data[i]); // 处理大量数据,阻塞UI
  }
}

// 优化后:使用setTimeout分解任务
function processLargeArrayAsync(data, chunkSize = 100) {
  let index = 0;

  function processChunk() {
    const end = Math.min(index + chunkSize, data.length);
    for (let i = index; i < end; i++) {
      processItem(data[i]);
    }
    index = end;

    if (index < data.length) {
      setTimeout(processChunk, 0); // 让出主线程
    }
  }

  processChunk();
}

面试要点

  1. 单线程与异步:理解JavaScript为什么是单线程以及如何实现异步
  2. 宏任务与微任务:能够区分常见的宏任务和微任务
  3. 执行顺序:掌握同步代码、微任务、宏任务的执行优先级
  4. async/await与事件循环:理解await会阻塞async函数内部代码,将其加入微任务队列
  5. 实际应用:能够分析复杂的代码执行顺序

常见考点

  • Promise.then是微任务,setTimeout是宏任务
  • await后面的代码会进入微任务队列
  • 微任务在当前宏任务结束后立即执行
  • 宏任务之间会检查并执行微任务队列