在 JavaScript 世界中,事件循环是一种以非阻塞方式执行异步操作的机制。了解事件循环对于开发 Web 应用程序至关重要,因为它支撑了 JavaScript 的异步特性,而异步特性是构建响应式高效 Web 应用程序的基础。
虽然这个概念乍一看似乎令人生畏,但我会将其分解,以便各个级别的开发人员都能找到有用的东西。
如果你不熟悉一些使用的术语,请不要担心 - 我会解释它们。但在深入研究这意味着什么之前,让我们首先了解 JavaScript 的工作原理以及我们为什么需要事件循环。
同步和异步分别是什么?
同步
同步(Synchronous)是一种编程模型,其中任务按顺序执行,每个任务必须等待前一个任务完成后才能开始。在同步编程中,程序会阻塞当前的执行流程,直到当前任务完成。这种模型适用于那些需要按特定顺序执行的任务,确保每个步骤都按预期顺序进行。
异步
异步(Asynchronous)是一种编程模型,它允许程序在等待某个操作完成时继续执行其他任务,而不是阻塞等待该操作的结果。这种机制在处理 I/O 操作(如文件读写、网络请求等)和定时任务时特别有用,因为它可以显著提高程序的效率和响应性。
为了更好地理解同步和异步的概念,可以考虑以下生活中的简单例子:
烹饪晚餐
假设你在厨房里准备晚餐,需要做以下几个任务:
- 煮米饭:需要20分钟。
- 炒菜:需要10分钟。
- 切水果:需要5分钟。
如果你按照同步的方式来做这些事情,你会依次进行每个任务:
- 先煮米饭(20分钟)
- 然后炒菜(10分钟)
- 最后切水果(5分钟)
这样总共需要 20 + 10 + 5 = 35 分钟。
但是,如果你采用异步的方式来处理这些任务,你可以同时进行多个任务:
- 首先开始煮米饭(20分钟),然后你不必一直站在锅边等米饭煮好。
- 在米饭煮的过程中,你可以开始炒菜(10分钟)。
- 在炒菜的同时,你还可以切水果(5分钟)。
这样一来,虽然煮米饭仍然需要20分钟,但在这段时间内,你已经完成了炒菜和切水果的任务。因此,总时间仍然是20分钟,而不是35分钟。
JavaScript 是单线程的
JavaScript 是单线程的,具有单个调用堆栈,但也支持异步操作,这使得它能够处理并发任务而不阻塞主执行线程。
让我们慢慢分析一下。简单来说:
- 单线程意味着 JavaScript 代码按顺序执行,每次执行一个命令。它只有一个主执行线程。
- JavaScript 只维护一个**调用栈,**它代表程序中调用的函数的执行上下文。每个函数调用都会创建一个被推送到调用栈的执行上下文。
- JavaScript 通过回调、promise、async/await 和计时器等机制支持异步编程。这些机制允许在后台调度和执行某些任务。
- 通过使用异步操作,JavaScript 可以有效地处理并发任务。这意味着当某些任务正在等待其异步操作完成时,主线程可以继续执行其他任务而不会被阻塞。
JavaScript 运行时
JavaScript 运行时可以看作是一个包含以下组件的“容器”:
-
JavaScript 引擎:负责解析、解释和执行 JavaScript 代码。
-
用于管理异步操作的两个主要队列:
- 回调队列(事件队列) :保存准备异步执行的任务,例如
setTimeout
和setInterval
的回调函数。 - 微任务队列(Promise 作业队列) :保存微任务,通常与
Promise
相关,如Promise
的then
和catch
回调。
- 回调队列(事件队列) :保存准备异步执行的任务,例如
-
Web API:由浏览器提供,允许 JavaScript 与各种浏览器功能交互,例如 DOM 操作、AJAX 请求和计时器。
这些组件的组合使得 JavaScript 能够高效地处理同步和异步任务,与浏览器环境进行交互,并管理并发操作。通过这种机制,JavaScript 可以在单线程环境中实现复杂的异步编程模型,同时保持良好的性能和响应性。
什么是 JavaScript 事件循环
实际的 JavaScript 事件循环机制虽然概念上相对简单,但它涉及三个关键概念,理解这些概念对于掌握事件循环的工作原理至关重要:
- 调用栈(Call Stack)
- Web API
- 回调队列
让我们逐一研究一下这些内容,看看它们是什么。
调用栈(Call Stack)
调用栈由 JavaScript 运行时(例如 V8)提供。作为单线程编程语言,Javascript 只有一个调用栈。这意味着一次只能执行一件事。
调用栈是一个 后进先出(LIFO) 的数据结构,用于存储当前正在执行的函数。每当一个函数被调用时,它会被推入调用栈;当函数执行完毕后,它会被从调用栈中弹出。
Web API
API 代表应用程序编程接口,它是与机器或系统交互的标准化方式。
当 JavaScript 代码在浏览器环境中执行时,JavaScript 引擎负责代码的解释和执行。对于超出核心 JavaScript 语言功能范围并需要与浏览器环境和外部资源进行交互的任务,Web API 便会发挥作用。
Web API 是由浏览器环境提供的功能,可通过 Web 浏览器中的全局 window
对象访问。这些 API 并非 JavaScript 原生提供的,但对于构建交互式和动态的 Web 应用程序至关重要。Web API 使开发人员能够执行各种任务,例如与 DOM 交互、使用计时器安排任务(如 setTimeout
和 setInterval
)、使用 fetch
发出网络请求以及处理 JSON 格式的数据。
这些 API 通常是非阻塞的,可以在后台异步执行。例如,setTimeout
会在指定的时间后将回调函数放入任务队列中。
任务队列(Callback Queue)
任务队列(又称宏任务队列或回调队列, )是一个先进先出(FIFO)的数据结构,用于存储异步任务的回调函数。
当这些异步任务启动时,Web API 会在后台处理它们,脱离常规的 JavaScript 执行流程。 当异步操作完成时,相应的回调函数会被放入任务队列或微任务队列。
Web API 主要处理异步任务,例如事件处理、网络请求和计时器。
常见任务:包括 setTimeout
、setInterval
、I/O 操作、事件处理程序等。
微任务队列(Microtask Queue)
微任务队列也是一个先进先出(FIFO)的数据结构,用于存储微任务的回调函数。
常见的微任务包括 Promise
的 then
和 catch
回调、MutationObserver
回调等。
微任务具有更高的优先级,会在当前宏任务结束后立即执行。
确保某些需要在当前宏任务结束后立即执行的任务得到及时处理,从而提高响应性。
事件循环
- 事件循环不断检查调用栈是否为空。
- 如果调用栈为空,事件循环会首先检查微任务队列,依次执行所有微任务,直到微任务队列为空。
- 然后,事件循环从任务队列中取出第一个宏任务并将其推入调用栈执行。
- 执行完一个宏任务后,事件循环再次检查微任务队列,重复上述过程。
console.log('start');
setTimeout(() => {
console.log('setTimeout function executed');
}, 0);
new Promise((resolve, reject) => {
resolve('Promise resolved');
})
.then((res) => console.log(res))
.catch((err) => console.log(err));
console.log('end');
上述代码片段的输出为
start
end
Promise resolved
setTimeout function executed
setTimeout()
我们可以看到,即使setTimeout()
有 0 秒时间并且已准备好执行,promise
也会在 之前执行。这是因为Promise
属于微任务队列的 优先于回调队列。
理解事件循环对我们有什么帮助?
异步操作
许多前端任务(如处理用户输入、发出 API 请求或为元素添加动画效果)都是异步的。理解事件循环有助于开发人员掌握如何在不阻塞主线程的情况下管理和执行这些操作,从而确保流畅的用户体验。
事件驱动架构
前端开发通常采用事件驱动架构,其中用户交互会触发需要异步处理的事件。了解事件在事件循环中的处理方式可以帮助开发人员编写能够有效响应用户操作和事件的代码。
优化性能
理解事件循环可以帮助前端开发人员编写更高效的代码。通过利用异步编程技术并适当安排任务,开发人员可以优化性能,确保关键任务优先处理,避免延迟或阻塞 UI。
调试和故障排除
了解事件循环的工作原理有助于开发人员更有效地调试和解决问题。通过分析异步任务和事件处理的执行顺序,开发人员可以更容易地识别瓶颈、竞争条件和其他性能问题。
框架和库
许多前端框架和库(如 React、Vue.js 和 Angular)都依赖于异步编程和事件驱动范式。理解事件循环可以使开发人员更有效地使用这些工具,并利用其功能构建可扩展且响应迅速的 Web 应用程序。
总的来说,对事件循环的透彻理解可以帮助开发人员编写更好的代码,优化性能,并构建响应更快、更高效的 Web 应用程序。
参考
What is the Event Loop in JavaScript and Why is it Essential to Understand?