JavaScript 事件循环详解

2025-06-04

JavaScript 事件循环详解

JavaScript 事件循环是需要理解的最重要的概念之一。它能帮助你理解 JavaScript 底层的工作原理。在本教程中,你将学习什么是 JavaScript 事件循环以及它是如何工作的。你还将学习一些关于调用堆栈、Web API 和消息队列的知识。

JavaScript 的构建块

JavaScript 中有几个基本构建块。这些块分别是内存堆堆栈调用堆栈Web API、消息队列和事件循环。内存堆是 JavaScript 存储对象和函数的地方。堆栈用于存储静态数据,例如原始数据类型的值。

调用堆栈是 JavaScript 用于跟踪需要执行的函数的一种机制。Web API 是内置于 Web 浏览器中的 API。这些 API 允许您使用原本无法使用的功能。例如 fetch API、地理位置 API、WebGL API、Web Workers API 等等。

这些 API 并非 JavaScript 语言本身的一部分。它们是构建于 JavaScript 核心语言之上的接口。这也是它们并非在所有 JavaScript 环境中都可用的原因。Web API 还处理异步方法,例如setTimeout和 事件。现在,我们来谈谈消息队列和事件循环。

消息队列

消息队列本质上是一个存储空间,用于存储 JavaScript 需要处理的“消息”。这些消息本质上都是回调函数,用于异步函数(例如setTimeout),以及用户触发的事件(例如点击事件和键盘事件)。

当任何异步函数执行或事件发生时,JavaScript 会首先将它们发送到调用堆栈。之后,JavaScript 会将每个函数或事件发送到相应的 Web API 进行处理。API 执行完所需操作后,会将一条包含相关回调函数的消息发送到消息队列。

这些消息会存储在消息队列中,直到调用栈为空。当调用栈为空时,队列中的第一条消息(回调)将被推送到调用栈。调用栈将执行该回调及其包含的代码。

关于消息队列,有一点很重要。调用栈遵循后进先出(LIFO)原则。这意味着最后一个被推送到调用栈的函数将被优先处理。消息队列不遵循此原则。在消息队列中,第一个消息或回调将被优先处理。

消息队列工作原理的一个简单示例

让我们用这个setTimeout方法演示一下。当你使用这个setTimeout方法时,JavaScript 会将其发送到执行它的调用堆栈。执行它会创建一个新的计时器。这个计时器会被发送到相应的 Web API。然后,该 API 会开始倒计时。

当倒计时归零时,API 会将该setTimeout方法的回调发送到消息队列。回调会在消息队列中等待,直到调用栈为空。当调用栈为空时,JavaScript 会从消息队列中取出回调并将其推送到调用栈,然后执行它。

// Use setTimeout method to delay
// execution of some function
setTimeout(function cb() {
  console.log('Hello.')
}, 500)

// Step 1:
// Add to call stack: setTimeout(function cb() { console.log('Hello.') }, 500)

// Call stack                                         //
// setTimeout(function cb() { console.log('Hello.') } //
//                                                    //

// Step 2:
// Send cb() to web API
// and remove setTimeout from call stack
// and create timer: 500

// Call stack //
//            //
//            //

// web API     //
// timer, cb() //
//             //

// Step 3:
// When timer is up, send cb() to message queue
// and remove it from web API

// web API     //
//             //
//             //

// message queue //
// cb()          //
//               //

// Step 4:
// When call stack is empty, send cb() to call stack
// and remove it from message queue

// message queue //
//               //
//               //

// Call stack //
// cb()       //
//            //
Enter fullscreen mode Exit fullscreen mode

调用堆栈、消息队列和优先级

在 JavaScript 中,调用堆栈和消息队列具有不同的优先级。调用堆栈的优先级高于消息队列的优先级。因此,消息队列必须等到调用堆栈为空后才能将任何内容从队列推送到调用堆栈。

只有当调用堆栈为空时,消息队列才能推送第一条消息或回调。这种情况何时发生?当调用堆栈中的所有函数调用以及这些调用的调用堆栈都执行完毕后,调用堆栈才会清空。此时,调用堆栈将为空,并可用于消息队列。

消息队列处理和零延迟

消息队列一次只能处理一条消息。此外,如果消息队列包含多条消息,则每条消息都必须先处理完毕,然后才能处理其他消息。每条消息的处理都依赖于前一条消息的完成。如果一条消息的处理时间较长,则其他消息必须等待。

这个原则叫做“运行至完成”。它还有另一个含义,叫做“零延迟”。假设你使用setTimeout方法并将延迟设置为 0。这样做的目的是,传入这个超时时间的回调应该立即执行。但实际情况是,这可能不会发生。

众所周知,消息队列一次只能处理一条消息。每条消息必须处理完毕后,队列才能处理下一条消息。因此,如果将setTimeout延迟设置为 0,则只有当它是消息队列中的第一条消息时,其回调才会立即执行。否则,它必须等待。

JavaScript 事件循环

这就是 JavaScript 处理异步操作的方式。这就是操作在调用堆栈、Web API 和消息队列之间传递的方式。尽管 JavaScript 本身是单线程的,但它能够做到这一点,因为 Web API 在单独的线程上运行。JavaScript 事件循环与此有何关系?

JavaScript 事件循环负责这个循环。JavaScript 事件循环的工作是不断检查调用栈是否为空。如果为空,它会从消息队列中取出第一条消息并将其推送到调用栈。

如果调用栈不为空,事件循环将不会让队列中的任何消息进入。相反,它会让调用栈处理其内部的调用。事件循环的每一次循环(或迭代)被称为“tick”。

关于承诺和异步函数的说明

异步方法(例如setTimeout和 事件)由 Web API 和消息队列处理。这不适用于异步函数Promise。异步函数和 Promise 由另一个队列处理。这个队列称为作业队列。该队列的另一个名称是微任务队列。

因此,当你使用 Promise 或异步函数时,setTimeout它们的处理方式会有所不同。首先,Promise 和异步函数将由任务队列处理,而异步函数setTimeout将由消息队列处理。其次,任务队列的优先级高于消息队列。这有一个重要的含义。

假设你有一个 Promise 和一个setTimeout。Promise 会立即解析,并且setTimeout延迟设置为 0。因此,它也应该立即执行 +/- 操作。为了更有趣,我们再添加一个常规函数。这个函数将放在最后。这会是什么结果呢?

第一个执行的函数是我们放在最后一个的常规函数​​。接下来将执行该 Promise 的任何回调。该回调将作为最后一个执行。在代码中,该方法是否位于 Promise 之上setTimeout无关紧要。setTimeout

重要的是,任务队列的优先级高于消息队列。因此,当 Promise 和setTimeoutThread 之间发生竞争时,Promise 最终会胜出。

// Create a function
function myFuncOne() {
  console.log('myFuncOne in setTimeout.')
}

// Create another function
function myFuncTwo() {
  console.log('myFuncTwo after the promise.')
}

// Delay the myFuncOne() by 0 seconds
setTimeout(myFuncOne, 0)

// Create a promise and resolve it immediately
new Promise((resolve, reject) => {
  resolve('Message from a promise')
})
  .then(res => console.log(res))

// Call the myFuncTwo()
myFuncTwo()

// Output:
// 'myFuncTwo after the promise.'
// 'Message from a promise'
// 'myFuncOne in setTimeout.'
Enter fullscreen mode Exit fullscreen mode

结论:JavaScript 事件循环解释

了解 JavaScript 事件循环有助于您理解 JavaScript 的底层工作原理。要理解这一点,您还需要了解调用堆栈、Web API 和消息队列等主题。我希望本教程能帮助您理解所有这些主题,以及最重要的 JavaScript 事件循环。

文章来源:https://dev.to/alexdevero/the-javascript-event-loop-explained-4be5
PREV
JavaScript 中的垃圾回收是什么以及它是如何工作的
NEXT
JavaScript 中创建对象的六种方法