前端JS: JavaScript 事件循环

8 阅读2分钟

JavaScript 事件循环 (Event Loop) 原理

核心概念

事件循环是JavaScript实现异步编程的关键机制,让单线程的JavaScript能够处理非阻塞I/O操作。

基本架构

调用栈 (Call Stack) ← 微任务队列 (Microtask Queue)
    ↓                      ↑
消息队列  (Task Queue)     ↑
    ↓                      ↑
事件循环 (Event Loop) ——→

主要组成部分

1. 调用栈 (Call Stack)

  • 后进先出(LIFO)结构
  • 存储执行上下文
  • 同步代码执行的位置

2. 堆 (Heap)

  • 内存分配区域
  • 存储对象、函数等引用类型

3. 队列 (Queue)

任务队列 (Task Queue / MacroTask Queue)
  • 宏任务:setTimeout、setInterval、I/O、UI渲染、事件回调
  • 一次事件循环处理一个宏任务
微任务队列 (Microtask Queue)
  • 微任务:Promise.then/catch/finally、MutationObserver、queueMicrotask
  • 优先级高于宏任务

事件循环执行顺序

  1. 执行同步代码(调用栈)
  2. 清空微任务队列
  3. 执行一个宏任务
  4. 再次清空微任务队列
  5. UI渲染(如果需要)
  6. 重复步骤3-5

代码示例

console.log('1. 同步代码开始'); // 同步任务

setTimeout(() => {
    console.log('4. 宏任务执行');
}, 0);

Promise.resolve().then(() => {
    console.log('3. 微任务执行');
});

console.log('2. 同步代码结束');

// 执行顺序:
// 1. 同步代码开始
// 2. 同步代码结束
// 3. 微任务执行
// 4. 宏任务执行

浏览器 vs Node.js

浏览器环境

  • 基于HTML规范
  • 每个浏览器标签页独立的事件循环
  • 宏任务:script代码块、setTimeout、事件回调、I/O

Node.js环境

  • 基于libuv库

  • 更复杂的事件循环阶段:

    timers → pending callbacks → idle, prepare → poll → checkclose callbacks
    
  • 包含process.nextTick队列(优先级最高)

实战注意事项

  1. 避免长时间同步任务:会阻塞事件循环
  2. 微任务递归:可能导致无限递归
  3. 合理使用异步:I/O密集型操作用异步API
  4. Web Workers:CPU密集型任务可使用Worker

性能优化建议

  • 将耗时操作分解为小任务
  • 使用requestAnimationFrame优化动画
  • 合理使用防抖和节流
  • 避免在微任务中执行耗时操作

常见面试题

// 经典面试题
setTimeout(() => console.log(1), 0);

Promise.resolve().then(() => console.log(2));

console.log(3);

// 输出:3 2 1

理解事件循环原理对于编写高效、无阻塞的JavaScript代码至关重要,特别是在处理异步操作、动画和用户交互时。