javascript

深入探讨 JavaScript 的事件循环

示例讲解javascript 事件循环机制

2024-11-26·阅读约 8 分钟·计算中...

深入探讨 JavaScript 的事件循环 🎡

在 JavaScript 开发中,事件循环(Event Loop) 是一个既神秘又重要的概念。掌握它不仅能帮助你理解 JavaScript 的运行机制,还能让你更好地优化代码性能、避免常见的异步陷阱。今天我们就从事件循环的核心逻辑出发,通过示例逐步揭开它的神秘面纱!✨


什么是事件循环? 🤔

简单来说,JavaScript 是单线程的,主线程一次只能处理一件事情。但是为了应对用户交互、API 调用等异步操作,它通过事件循环的机制,让这些任务井然有序地执行。

事件循环的核心任务就是:

  1. 先执行同步代码(直接加入调用栈)。
  2. 等待异步任务完成,将其回调放入合适的队列。
  3. 检查调用栈是否为空,空了就处理队列中的任务。

你可以将事件循环想象成一位忙碌的主持人 🎤,总是在调用栈和队列之间来回奔波,确保所有任务都被妥善安排。


事件循环的关键组成部分

  1. 调用栈(Call Stack)

    • 用于存储待执行的函数。函数被调用时压入栈顶,执行完毕后弹出栈。
  2. Web APIs / 异步任务处理器

    • 浏览器或 Node.js 提供的异步操作机制,比如 setTimeout、HTTP 请求、事件监听等。
  3. 任务队列(Task Queue)

    • 包含两种队列:
      • 宏任务(Macrotask)setTimeoutsetInterval、DOM 操作等。
      • 微任务(Microtask)Promise 回调、queueMicrotask 等。
    • 微任务的优先级高于宏任务。

代码示例:同步 vs 异步

同步代码
console.log("1️⃣ 开始做饭 🍳");
console.log("2️⃣ 吃早餐 🍴");
console.log("3️⃣ 洗碗 🧼");

输出:

1️⃣ 开始做饭 🍳
2️⃣ 吃早餐 🍴
3️⃣ 洗碗 🧼

解析: 代码按顺序执行,没有异步任务参与。


异步代码:setTimeout 示例
console.log("1️⃣ 开始做饭 🍳");

setTimeout(() => {
  console.log("2️⃣ 吃早餐 🍴(延迟 3 秒)");
}, 3000);

console.log("3️⃣ 洗碗 🧼");

输出:

1️⃣ 开始做饭 🍳
3️⃣ 洗碗 🧼
2️⃣ 吃早餐 🍴(延迟 3 秒)

解析:

  • setTimeout 的回调任务被交给 Web API 处理,并在 3 秒后放入宏任务队列。
  • 主线程继续执行同步代码,打印 "洗碗 🧼" 后才检查宏任务队列。

微任务优先级:Promise 示例

console.log("1️⃣ 开始 🍳");

setTimeout(() => {
  console.log("2️⃣ 宏任务:setTimeout ⏳");
}, 0);

Promise.resolve().then(() => {
  console.log("3️⃣ 微任务:Promise ✅");
});

console.log("4️⃣ 结束 🚀");

输出:

1️⃣ 开始 🍳
4️⃣ 结束 🚀
3️⃣ 微任务:Promise ✅
2️⃣ 宏任务:setTimeout ⏳

解析:

  • Promise 回调是微任务,优先于 setTimeout 这类宏任务执行。
  • 即便 setTimeout 的延迟是 0ms,微任务也会先执行。

处理繁重任务:分块执行

当 JavaScript 遇到耗时操作时,比如复杂的循环或庞大的计算,它可能阻塞主线程,导致页面卡顿。这时,我们可以利用异步机制将任务分块处理。

坏示例:阻塞主线程
console.log("1️⃣ 开始 🏁");

for (let i = 0; i < 1e9; i++) {}  // 模拟繁重任务

console.log("2️⃣ 结束 🛑");

执行时,页面可能会冻结,直到循环结束。

好示例:分块执行
console.log("1️⃣ 开始 🏁");

let count = 0;

function heavyTask() {
  if (count < 1e6) {
    count++;
    if (count % 100000 === 0) console.log(`已处理 ${count} 项 🔄`);
    setTimeout(heavyTask, 0);  // 让事件循环喘口气!
  } else {
    console.log("2️⃣ 任务完成 ✅");
  }
}

heavyTask();

解析:

  • 每次处理一小块任务后,利用 setTimeout 让事件循环有时间处理其他任务,避免页面卡顿。

小测试:你掌握了吗?

console.log("1️⃣ Hello 👋");

setTimeout(() => {
  console.log("2️⃣ Timeout ⏳");
}, 0);

Promise.resolve().then(() => {
  console.log("3️⃣ Promise ✅");
});

console.log("4️⃣ Goodbye 👋");

问题:输出的顺序是?

A. 1️⃣ Hello, 2️⃣ Timeout, 3️⃣ Promise, 4️⃣ Goodbye
B. 1️⃣ Hello, 4️⃣ Goodbye, 3️⃣ Promise, 2️⃣ Timeout
C. 1️⃣ Hello, 3️⃣ Promise, 4️⃣ Goodbye, 2️⃣ Timeout

答案: C

  • 同步代码 1️⃣ Hello4️⃣ Goodbye 先执行。
  • 微任务 3️⃣ Promise 紧随其后。
  • 最后执行宏任务 2️⃣ Timeout

总结:事件循环的精髓

1️⃣ 同步任务优先: 主线程按顺序执行代码。
2️⃣ 异步任务由事件循环调度: 包括 Web API 和任务队列。
3️⃣ 微任务优先级更高: 比如 Promise,在宏任务之前执行。
4️⃣ 优化代码性能: 使用异步方式分块处理繁重任务,保持应用流畅。


你的看法?

事件循环是 JavaScript 异步编程的核心,理解它能让你更轻松地解决回调地狱、性能瓶颈等问题。如果你有其他问题或疑惑,欢迎留言讨论! 💬

觉得有帮助的话,分享给更多开发者吧!🌟

订阅 FreeMac

每周精选:Mac 高效技巧、免费替代付费软件、开发者工具推荐。用对你的 MacBook,省钱 + 提效。