likes
comments
collection
share

JavaScript 的事件循环 (Event Loop),读完你就明白了一文带你读懂什么是事件循环,事件循环是 Jav

作者站长头像
站长
· 阅读数 42

JavaScript 是一种单线程的编程语言,这意味着它一次只能执行一个任务。为了管理复杂的异步操作,如事件处理、定时器、网络请求等,JavaScript 依赖于事件循环 (Event Loop) 来调度任务,确保非阻塞的执行。本文将详细介绍 JavaScript 的事件循环机制,解释其工作原理,并探讨如何在实际开发中应用事件循环。

一、什么是事件循环?

事件循环是 JavaScript 中处理异步操作的核心机制。它的主要职责是监控调用栈 (Call Stack) 和任务队列 (Task Queue),并协调两者的工作,以确保 JavaScript 程序能够高效地处理异步任务,而不会阻塞主线程。

二、事件循环的工作原理

要理解事件循环的工作原理,首先需要了解以下几个关键概念:

1. 调用栈 (Call Stack)

调用栈是一种 LIFO(后进先出)结构,用于存储和跟踪函数的调用。每当一个函数被调用时,它会被压入栈顶,函数执行完成后,函数就会从栈顶弹出。

function foo() {
  console.log("foo");
}

function bar() {
  foo();
  console.log("bar");
}

bar();

在这个示例中,调用栈的变化如下:

  • bar() 被调用,bar 压入栈顶
  • bar 调用了 foofoo 压入栈顶
  • foo 执行完成并从栈顶弹出。
  • bar 执行完成并从栈顶弹出。

2. 任务队列 (Task Queue)

任务队列是一个 FIFO(先进先出)结构,用于存储异步任务的回调函数。当异步操作(如定时器、事件、网络请求等)完成后,其回调函数会被放入任务队列中,等待调用栈清空后再执行。

任务队列通常分为两类:

  • 宏任务队列 (Macro Task Queue): 包括 setTimeoutsetIntervalI/Oscript 等任务。
  • 微任务队列 (Micro Task Queue): 包括 Promise.thenprocess.nextTick(Node.js 环境)等任务。

3. 事件循环 (Event Loop)

事件循环的核心工作就是检查调用栈是否为空,并在栈为空时,从任务队列中取出下一个任务并执行。如果任务队列中有微任务,事件循环会优先处理微任务队列中的任务,然后再处理宏任务队列中的任务。

事件循环的步骤:

  • 1、检查调用栈是否为空。
  • 2、如果调用栈为空,检查微任务队列是否有任务,如果有,则依次执行。
  • 3、微任务队列为空后,从宏任务队列中取出第一个任务,并将其回调函数压入调用栈执行。
  • 4、重复上述步骤。

三、事件循环的具体示例

通过以下几个示例,深入理解事件循环的工作机制。

1. 基本示例

console.log("Start");

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

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

console.log("End");

执行顺序分析:

  • 1、console.log('Start') 被执行,输出 "Start"。
  • 2、setTimeout 的回调被注册,加入宏任务队列。
  • 3、Promise.resolve().then 的回调被注册,加入微任务队列。
  • 4、console.log('End') 被执行,输出 "End"。
  • 5、调用栈为空,事件循环检查微任务队列,执行 Promise.then 回调,输出 "Promise"。
  • 6、微任务队列为空,事件循环从宏任务队列中取出 setTimeout 回调并执行,输出 "Timeout"。

最终输出顺序为:"Start" -> "End" -> "Promise" -> "Timeout"。

2. 微任务优先于宏任务

setTimeout(() => {
  console.log("Macro Task 1");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("Micro Task 1");
  })
  .then(() => {
    console.log("Micro Task 2");
  });

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

执行顺序分析:

  • 1、两个 setTimeout 回调被加入宏任务队列。
  • 2、Promise.then 回调被加入微任务队列。
  • 3、 调用栈清空后,事件循环优先处理微任务队列,依次执行 "Micro Task 1" 和 "Micro Task 2"。
  • 4、微任务队列为空后,事件循环依次处理宏任务队列中的任务,输出 "Macro Task 1" 和 "Macro Task 2"。

最终输出顺序为:"Micro Task 1" -> "Micro Task 2" -> "Macro Task 1" -> "Macro Task 2"。

3. 异步任务的嵌套

console.log("Start");

setTimeout(() => {
  console.log("Timeout 1");
  Promise.resolve().then(() => {
    console.log("Promise 1");
  });
}, 0);

Promise.resolve().then(() => {
  console.log("Promise 2");
  setTimeout(() => {
    console.log("Timeout 2");
  }, 0);
});

console.log("End");

执行顺序分析:

  • 1、输出 "Start" 和 "End"。
  • 2、事件循环检查微任务队列,执行 "Promise 2"。
  • 3、"Promise 2" 的 then 回调中注册了一个宏任务 Timeout 2
  • 4、处理宏任务队列,执行 "Timeout 1"。
  • 5、"Timeout 1" 的回调中注册了一个微任务 "Promise 1"。
  • 6、处理微任务 "Promise 1"。
  • 7、最后处理宏任务 "Timeout 2"。

最终输出顺序为:"Start" -> "End" -> "Promise 2" -> "Timeout 1" -> "Promise 1" -> "Timeout 2"。

四、事件循环在实际开发中的应用

在实际开发中,事件循环的理解和应用对处理异步任务、优化性能以及避免常见错误至关重要。

1. 避免 UI 阻塞

在浏览器环境中,JavaScript 在主线程上运行,这意味着长时间的同步操作会阻塞 UI 渲染,导致页面卡顿。通过将耗时操作放入异步任务中,可以避免阻塞主线程。

function heavyComputation() {
  // 假设这是一个耗时的同步操作
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  }
  console.log(sum);
}

setTimeout(heavyComputation, 0);
console.log("UI is still responsive");

通过将 heavyComputation 放入 setTimeout 中,确保了 UI 的响应性。

2. 正确处理异步操作

在处理异步操作时,理解事件循环有助于正确处理任务的执行顺序,避免一些常见的陷阱,例如错误地假设异步任务会按照特定顺序执行。

function asyncTask() {
  setTimeout(() => console.log("Task 1"), 0);
  Promise.resolve().then(() => console.log("Task 2"));
  console.log("Task 3");
}

asyncTask();

输出顺序为:"Task 3" -> "Task 2" -> "Task 1"。理解事件循环可以帮助我们正确预测和调试这种异步行为。

3. 优化性能与用户体验

事件循环的机制可以用来优化性能。例如,分割长时间运行的任务,使得主线程能够更频繁地处理用户交互和更新 UI。

function processLargeArray(arr) {
  if (arr.length === 0) return;

  setTimeout(() => {
    const chunk = arr.splice(0, 100);
    chunk.forEach((item) => console.log(item));

    processLargeArray(arr);
  }, 0);
}

const largeArray = new Array(10000).fill(0).map((_, i) => i + 1);
processLargeArray(largeArray);

这种“分片”处理可以避免长时间运行的任务阻塞主线程,从而提升用户体验。

五、总结

JavaScript 的事件循环是理解异步编程的关键。通过事件循环,JavaScript 能够在单线程环境下高效地处理异步任务,而不阻塞主线程。本文详细介绍了事件循环的工作机制,并通过具体示例展示了它在实际开发中的应用。

掌握事件循环能够帮助我们编写更加高效、响应更快的代码,尤其是在处理复杂的异步逻辑时,理解事件循环是确保代码正确性的重要基础。

P.S.

本文首发于我的个人网站www.aifeir.com,若你觉得有所帮助,可以点个爱心鼓励一下,如果大家喜欢这篇文章,希望多多转发分享,感谢大家的阅读。

转载自:https://juejin.cn/post/7405523061891923979
评论
请登录