事件循环

JavaScript 有一个基于事件循环的运行时模型,它负责执行代码、收集和处理事件以及执行排队的子任务。该模型与 C 和 Java 等其他语言中的模型有很大不同。

¥JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java.

运行时概念

¥Runtime concepts

以下部分解释了理论模型。现代 JavaScript 引擎实现并大量优化了所描述的语义。

¥The following sections explain a theoretical model. Modern JavaScript engines implement and heavily optimize the described semantics.

视觉表现

¥Visual representation

A diagram showing how stacks are comprised of frames, heaps are comprised of objects, and queues are comprised of messages.

¥Stack

函数调用形成帧堆栈。

¥Function calls form a stack of frames.

js
function foo(b) {
  const a = 10;
  return a + b + 11;
}

function bar(x) {
  const y = 3;
  return foo(x * y);
}

const baz = bar(7); // assigns 42 to baz

操作顺序:

¥Order of operations:

  1. 当调用 bar 时,会创建第一个帧,其中包含对 bar 的参数和局部变量的引用。
  2. bar 调用 foo 时,会创建第二个框架并将其推送到第一个框架的顶部,其中包含对 foo 的参数和局部变量的引用。
  3. foo 返回时,顶部帧元素被弹出堆栈(只留下 bar 的调用帧)。
  4. bar 返回时,堆栈为空。

请注意,参数和局部变量可能会继续存在,因为它们存储在堆栈之外 - 因此在其外部函数返回后很长时间内,任何 嵌套函数 都可以访问它们。

¥Note that the arguments and local variables may continue to exist, as they are stored outside the stack — so they can be accessed by any nested functions long after their outer function has returned.

¥Heap

对象被分配在堆中,堆只是一个名称,表示一个大的(大部分是非结构化的)内存区域。

¥Objects are allocated in a heap which is just a name to denote a large (mostly unstructured) region of memory.

队列

¥Queue

JavaScript 运行时使用消息队列,它是要处理的消息的列表。每条消息都有一个关联的函数,调用该函数来处理该消息。

¥A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message.

事件循环 期间的某个时刻,运行时开始处理队列上的消息,从最旧的消息开始。为此,该消息将从队列中删除,并使用该消息作为输入参数来调用其相应的函数。与往常一样,调用函数会创建一个新的堆栈帧供该函数使用。

¥At some point during the event loop, the runtime starts handling the messages on the queue, starting with the oldest one. To do so, the message is removed from the queue and its corresponding function is called with the message as an input parameter. As always, calling a function creates a new stack frame for that function's use.

函数的处理继续进行,直到堆栈再次为空。然后,事件循环将处理队列中的下一条消息(如果有)。

¥The processing of functions continues until the stack is once again empty. Then, the event loop will process the next message in the queue (if there is one).

事件循环

¥Event loop

事件循环因其通常的实现方式而得名,通常类似于:

¥The event loop got its name because of how it's usually implemented, which usually resembles:

js
while (queue.waitForMessage()) {
  queue.processNextMessage();
}

queue.waitForMessage() 同步等待消息到达(如果消息尚未可用并等待处理)。

¥queue.waitForMessage() waits synchronously for a message to arrive (if one is not already available and waiting to be handled).

"运行至完成"

¥"Run-to-completion"

每条消息都会在处理任何其他消息之前完成处理。

¥Each message is processed completely before any other message is processed.

在推断程序时,这提供了一些很好的属性,包括每当函数运行时,它都不能被抢占,并且将在任何其他代码运行之前完全运行(并且可以修改函数操作的数据)。这与 C 不同,例如,如果一个函数在线程中运行,则运行时系统可能会在任何时候停止它以在另一个线程中运行其他代码。

¥This offers some nice properties when reasoning about your program, including the fact that whenever a function runs, it cannot be preempted and will run entirely before any other code runs (and can modify data the function manipulates). This differs from C, for instance, where if a function runs in a thread, it may be stopped at any point by the runtime system to run some other code in another thread.

此模型的缺点是,如果消息需要很长时间才能完成,Web 应用将无法处理单击或滚动等用户交互。浏览器通过 "脚本运行时间太长" 对话框缓解了这种情况。遵循的一个好习惯是缩短消息处理时间,如果可能的话将一条消息削减为多条消息。

¥A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll. The browser mitigates this with the "a script is taking too long to run" dialog. A good practice to follow is to make message processing short and if possible cut down one message into several messages.

添加消息

¥Adding messages

在 Web 浏览器中,每当事件发生时就会添加消息,并且附加了一个事件监听器。如果没有监听器,事件就会丢失。因此,单击具有单击事件处理程序的元素将添加一条消息 - 与任何其他事件类似。

¥In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost. So a click on an element with a click event handler will add a message — likewise with any other event.

函数 setTimeout 的前两个参数是要添加到队列的消息和时间值(可选;默认为 0)。时间值表示消息将被推入队列的(最小)延迟。如果队列中没有其他消息,并且堆栈为空,则在延迟后立即处理该消息。但是,如果有消息,setTimeout 消息将不得不等待其他消息被处理。因此,第二个参数表示最短时间,而不是保证时间。

¥The first two arguments to the function setTimeout are a message to add to the queue and a time value (optional; defaults to 0). The time value represents the (minimum) delay after which the message will be pushed into the queue. If there is no other message in the queue, and the stack is empty, the message is processed right after the delay. However, if there are messages, the setTimeout message will have to wait for other messages to be processed. For this reason, the second argument indicates a minimum time — not a guaranteed time.

下面是一个演示此概念的示例(setTimeout 在计时器到期后不会立即运行):

¥Here is an example that demonstrates this concept (setTimeout does not run immediately after its timer expires):

js
const seconds = new Date().getTime() / 1000;

setTimeout(() => {
  // prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
  console.log(`Ran after ${new Date().getTime() / 1000 - seconds} seconds`);
}, 500);

while (true) {
  if (new Date().getTime() / 1000 - seconds >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

零延误

¥Zero delays

零延迟并不意味着回调将在零毫秒后触发。延迟 0(零)毫秒调用 setTimeout 不会在给定时间间隔后执行回调函数。

¥Zero delay doesn't mean the call back will fire-off after zero milliseconds. Calling setTimeout with a delay of 0 (zero) milliseconds doesn't execute the callback function after the given interval.

执行取决于队列中等待任务的数量。在下面的示例中,消息 "this is just a message" 将在回调中的消息被处理之前写入控制台,因为延迟是运行时处理请求所需的最短时间(不是保证时间)。

¥The execution depends on the number of waiting tasks in the queue. In the example below, the message "this is just a message" will be written to the console before the message in the callback gets processed, because the delay is the minimum time required for the runtime to process the request (not a guaranteed time).

即使你为 setTimeout 指定了特定的时间限制,setTimeout 也需要等待排队消息的所有代码完成。

¥The setTimeout needs to wait for all the code for queued messages to complete even though you specified a particular time limit for your setTimeout.

js
(() => {
  console.log("this is the start");

  setTimeout(() => {
    console.log("Callback 1: this is a msg from call back");
  }); // has a default time value of 0

  console.log("this is just a message");

  setTimeout(() => {
    console.log("Callback 2: this is a msg from call back");
  }, 0);

  console.log("this is the end");
})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

多个运行时一起通信

¥Several runtimes communicating together

Web Worker 或跨域 iframe 有自己的堆栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法发送消息来进行通信。如果另一个运行时监听 message 事件,则此方法会向另一个运行时添加一条消息。

¥A web worker or a cross-origin iframe has its own stack, heap, and message queue. Two distinct runtimes can only communicate through sending messages via the postMessage method. This method adds a message to the other runtime if the latter listens to message events.

永不阻塞

¥Never blocking

事件循环模型的一个非常有趣的属性是,与许多其他语言不同,JavaScript 永远不会阻塞。处理 I/O 通常通过事件和回调执行,因此当应用等待 IndexedDB 查询返回或 fetch() 请求返回时,它仍然可以处理其他事务,例如用户输入。

¥A very interesting property of the event loop model is that JavaScript, unlike a lot of other languages, never blocks. Handling I/O is typically performed via events and callbacks, so when the application is waiting for an IndexedDB query to return or a fetch() request to return, it can still process other things like user input.

存在像 alert 或同步 XHR 这样的遗留异常,但避免它们被认为是很好的做法。谨防:例外的例外确实存在(但通常是实现错误,而不是其他任何错误)。

¥Legacy exceptions exist like alert or synchronous XHR, but it is considered good practice to avoid them. Beware: exceptions to the exception do exist (but are usually implementation bugs, rather than anything else).

也可以看看

¥See also