event loop 事件循环

什么是事件循环?

事件循环是 JavaScript 运行时的一个核心机制,它管理着代码的执行顺序。它是一种机制,用于处理异步操作,事件循环的核心是一个循环,它不断地检查调用栈和任务队列,以确保代码按照正确的顺序执行。

JavaScript 的单线程本质

JavaScript 被设计为单线程语言,这意味着它只有一个调用栈,一次只能执行一段代码。这听起来像是一个限制,但正是这种简单性让 JavaScript 如此易于使用。

javascript 复制代码
console.log('开始'); // 1

setTimeout(() => {
  console.log('定时器回调'); // 3
}, 1000);

console.log('结束'); // 2

// 输出顺序:
// 开始
// 结束
// 定时器回调

事件循环的组成部分

1. 调用栈(Call Stack)

调用栈是 JavaScript 执行代码的地方。当函数被调用时,它会被推入栈顶;当函数返回时,它会从栈顶弹出。

javascript 复制代码
function first() {
  console.log('第一个函数');
  second();
}

function second() {
  console.log('第二个函数');
}

first();

2. 任务队列(Task Queue)

任务队列(也称为宏任务队列)存储着待处理的任务,如:

  • setTimeoutsetInterval 回调
  • I/O 操作
  • UI 渲染
  • 事件处理程序

3. 微任务队列(Microtask Queue)

微任务队列具有更高的优先级,包括:

  • Promise 回调(.then(), .catch(), .finally()
  • queueMicrotask()
  • MutationObserver

事件循环的工作流程

事件循环遵循一个简单的循环:

  1. 执行调用栈中的同步代码
  2. 当调用栈为空时,检查微任务队列
  3. 执行所有微任务(直到微任务队列为空)
  4. 检查宏任务队列 ,执行一个宏任务
  5. 重复步骤 2-4
javascript 复制代码
console.log('脚本开始'); // 同步代码

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

Promise.resolve()
  .then(() => {
    console.log('Promise 1'); // 微任务
  })
  .then(() => {
    console.log('Promise 2'); // 微任务
  });

console.log('脚本结束'); // 同步代码

// 输出顺序:
// 脚本开始
// 脚本结束
// Promise 1
// Promise 2
// setTimeout

实际应用示例

场景 1:用户交互与数据获取

javascript 复制代码
// 模拟用户点击和API调用
document.getElementById('button').addEventListener('click', () => {
  console.log('点击事件处理'); // 宏任务
  
  // 微任务优先于渲染
  Promise.resolve().then(() => {
    console.log('Promise 在点击中');
  });
  
  // 模拟API调用
  fetch('/api/data')
    .then(response => response.json())
    .then(data => {
      console.log('获取到的数据:', data); // 微任务
    });
});

console.log('脚本加载完成');

场景 2:动画性能优化

javascript 复制代码
// 不推荐的写法 - 可能阻塞渲染
function processHeavyData() {
  const data = Array.from({length: 100000}, (_, i) => i);
  return data.map(x => Math.sqrt(x)).filter(x => x > 10);
}

// 推荐的写法 - 使用事件循环分块处理
function processInChunks(data, chunkSize = 1000) {
  let index = 0;
  
  function processChunk() {
    const chunk = data.slice(index, index + chunkSize);
    
    // 处理当前块
    chunk.forEach(item => {
      // 处理逻辑
    });
    
    index += chunkSize;
    
    if (index < data.length) {
      // 使用 setTimeout 让出控制权,允许渲染
      setTimeout(processChunk, 0);
    }
  }
  
  processChunk();
}

常见陷阱与最佳实践

陷阱 1:阻塞事件循环

javascript 复制代码
// ❌ 避免 - 长时间运行的同步操作
function blockingOperation() {
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // 阻塞5秒
  }
  console.log('操作完成');
}

// ✅ 推荐 - 使用异步操作
async function nonBlockingOperation() {
  await new Promise(resolve => setTimeout(resolve, 5000));
  console.log('操作完成');
}

陷阱 2:微任务递归

javascript 复制代码
// ❌ 可能导致微任务无限循环
function dangerousRecursion() {
  Promise.resolve().then(dangerousRecursion);
}

// ✅ 使用 setImmediate 或 setTimeout 打破循环
function safeRecursion() {
  Promise.resolve().then(() => {
    setTimeout(safeRecursion, 0);
  });
}

现代 JavaScript 中的事件循环

async/await 与事件循环

javascript 复制代码
async function asyncExample() {
  console.log('开始 async 函数');
  
  await Promise.resolve();
  console.log('在 await 之后'); // 微任务
  
  const result = await fetch('/api/data');
  console.log('数据获取完成'); // 微任务
}

console.log('脚本开始');
asyncExample();
console.log('脚本结束');

// 输出顺序:
// 脚本开始
// 开始 async 函数
// 脚本结束
// 在 await 之后
// 数据获取完成

调试技巧

1. 使用 console 理解执行顺序

javascript 复制代码
console.log('同步 1');

setTimeout(() => console.log('宏任务 1'), 0);

Promise.resolve()
  .then(() => console.log('微任务 1'))
  .then(() => console.log('微任务 2'));

queueMicrotask(() => console.log('微任务 3'));

console.log('同步 2');

2. 性能监控

javascript 复制代码
// 测量任务执行时间
const startTime = performance.now();

setTimeout(() => {
  const endTime = performance.now();
  console.log(`任务执行耗时: ${endTime - startTime}ms`);
}, 0);

执行顺序问题

网上很经典的面试题

js 复制代码
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')

输出结果

js 复制代码
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

总结

理解 JavaScript 事件循环对于编写高效、响应迅速的应用程序至关重要。记住这些关键点:

  • 同步代码首先执行
  • 微任务在同步代码之后、渲染之前执行
  • 宏任务在微任务之后执行
  • 避免阻塞主线程
  • 合理使用微任务和宏任务

掌握事件循环机制将帮助你写出更好的异步代码,避免常见的性能问题,并创建更流畅的用户体验。

希望这篇博客能帮助你更好地理解 JavaScript 的事件循环机制!如果你有任何问题或想法,欢迎在评论区讨论。

相关推荐
Giant1003 小时前
教你用几行代码,在网页里调出前置摄像头!
javascript
AAA阿giao3 小时前
不用 JavaScript,你能用 CSS 做到什么?答案:拍一部星战电影!
前端·css
golang学习记3 小时前
从0死磕全栈之在 Next.js 中使用 Sass
前端
好大的月亮3 小时前
oss中的文件替换后chrome依旧下载到缓存文件概述
前端·chrome·缓存
Broken Arrows3 小时前
解决Jenkins在构建前端任务时报错error minimatch@10.0.3:……的记录
运维·前端·jenkins
明月与玄武3 小时前
JS 自定义事件:从 CustomEvent 到 dispatchEvent!
前端·javascript·vue.js
Zhencode3 小时前
vue之异步更新队列
前端·javascript·vue.js
九年义务漏网鲨鱼4 小时前
从零学习 Agentic RL(四)—— 超越 ReAct 的线性束缚:深入解析 Tree-of-Thoughts (ToT)
前端·学习·react.js
Jay丶4 小时前
Next.js 与 SEO:让搜索引擎爱上你的网站 💘
前端·javascript·react.js