Code前端首页关于Code前端联系我们

JavaScript  The event loop

terry 2年前 (2023-09-10) 阅读数 100 #前端教程

JavaScript  The event loop

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

运行时概念

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

JavaScript  The event loopJavaScript  The event loop

Stack

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

操作顺序:

调用 bar 时,会创建第一个frame,其中包含对 bar 的参数和局部变量的引用。

当 bar 调用 foo 时,将创建第二个frame并将其推到第一个框架之上,其中包含对 foo 的参数和局部变量的引用。

当 foo 返回时,顶部frame元素从堆栈中弹出(只留下 bar 的调用frame)。

当 bar 返回时,堆栈为空。

Heap

object被分配在堆中,堆只是一个名称,用于表示一块大的(大多不规则的)内存区域。

Queue

JavaScript 运行时使用消息队列,这是一个待处理消息列表。每个消息都有一个相关联的函数,该函数会被调用来处理该消息。

在事件循环的某个时刻,运行时开始处理消息队列中的消息,从最早的消息开始。为了这样做,消息被从队列中删除,并且相应的函数被调用,将该消息作为输入参数。调用函数总是会为该函数创建一个新的栈帧以供使用。

函数处理会一直持续到栈再次为空。然后,事件循环将处理队列中的下一个消息(如果有的话)。

Event loop

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

queue.waitForMessage() 同步等待消息到达(如果没有消息可用且正在等待处理)。

“Run-to-completion”

指执行过程中不会被中断,即该过程会一直运行到结束,不会在执行的过程中被其他程序或事件打断。这在单线程编程中特别常见,其中一个任务必须执行完成后才能顺序执行下一个任务。JavaScript 就是一个典型的单线程运行时环境,它在处理一个事件或任务时不会被打断,直到执行完成后才处理下一个事件或任务。

消息队列是 JavaScript 实现异步机制的重要部分。在 JavaScript 中,每当事件发生时,例如用户点击页面或发生定时器事件,就会产生一个消息(message)并被放入消息队列中。 JavaScript 引擎会处理消息队列中的全部消息,这样可以保证每个消息都被完全处理,避免发生竞争条件等问题。

由于 JavaScript 是单线程语言,线程只能一次处理一个任务。因此,在处理一个任务执行期间,不会有其他任务来打断它,这也就意味着,当一个函数运行时,它会一直运行直到完成,而不会被其他代码打断或影响数据的更改。

但是,这种模型也有缺点,在某些情况下,处理消息的时间可能过长,导致页面无法响应用户的交互事件,例如用户点击页面或发生滚动。为了解决这个问题,浏览器会弹出“脚本正在运行过程中”的对话框。为了避免这种情况的发生,编写 JavaScript 代码时应该使消息处理尽可能短,并尽可能将一个消息拆分成几个小的消息进行处理。

在 Web 浏览器中,当事件发生且存在事件监听器时,就会向消息队列中添加一条消息。如果没有监听器,该事件将会丢失。例如,当用户点击具有点击事件处理程序的元素时,将会添加一条消息。

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

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

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

将 setTimeout 函数的延迟时间设置为 0 毫秒并不意味着回调函数将在 0 毫秒后立即执行。

实际上,回调函数在执行之前要等待消息队列中所有已存在的任务执行完毕。因此,当设置 setTimeout 的延迟时间为 0 毫秒时,回调函数不会立即执行,而是要等待所有当前存在的任务执行完毕后才执行。

下面是一个示例,它演示了这个延迟行为。在该示例中,输出到控制台的“this is just a message”消息会在回调函数中的消息之前输出,因为 setTimeout 的延迟时间仅代表了运行时处理请求所需的最小时间,而不是保证的时间。

因此,需要注意的是,即使在设置 setTimeout 的回调函数延迟时间时指定了时间,但回调函数仍然需要等待队列中所有的任务都执行完毕后才会执行。

(() => {
  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 方法发送消息来进行通信。如果另一个运行时监听了消息事件,则该方法将向该运行时添加一条消息。

Never blocking

事件循环模型的一个非常有趣的特性是,与许多其他语言不同,JavaScript 永远不会被阻塞。处理 I/O 通常通过事件和回调函数来完成,因此当应用程序等待 IndexedDB 查询返回或 XHR 请求返回时,它仍然可以处理其他事情,例如用户输入等。

尽管 JavaScript 具有这种非阻塞特性,但是仍然存在特例,比如 alert 或同步 XHR。通常来说,应该避免使用这些特例。需要注意的是,特例的特例确实存在(但通常是实现错误而非其他原因所导致)。

举个例子:

console.log('Start');

setTimeout(function() {
  console.log('Timeout complete');
}, 1000);

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(function(response) {
    return response.json();
  })
  .then(function(json) {
    console.log('Fetch complete', json);
  });

console.log('End');

在这个例子中,我们使用了 setTimeout 函数来模拟一个耗时的操作(一秒钟)。我们还使用了 fetch 函数来发起 HTTP 请求。这两个操作都是非阻塞的,它们会在后台运行而不会阻塞 JavaScript 运行时的主线程。

在这段代码运行时,它将首先输出 ‘Start’,然后输出 ‘End’。这是因为 console.log 函数是同步的,它会立即输出对应的文本。

接下来,JavaScript 运行时会将 setTimeout 函数的回调函数添加到消息队列中,并立即执行下一行代码,即 fetch 操作。fetch 函数是异步的,它不会等待 HTTP 请求完成。

当 fetch 函数完成后,它将使用 Promise 对象来将回调函数添加到消息队列中,以便在未来的某个时间点运行。然后,JavaScript 运行时将继续执行下一行代码,即 console.log(‘End’)。

因为没有其他事件在消息队列中等待执行,JavaScript 运行时将暂停执行。然后,一秒钟后,setTimeout 函数的回调函数将被添加到消息队列中,JavaScript 运行时将取出该事件并执行对应的代码,即输出 ‘Timeout complete’。

最后,当 fetch 函数的回调函数准备好时,JavaScript 运行时将再次从消息队列中取出事件并执行对应的代码,即输出 ‘Fetch complete’。

这个例子演示了事件循环模型如何使 JavaScript 不被阻塞,因为在未来的某个时间点上,回调函数会被添加到消息队列中,并在 JavaScript 运行时的主线程可用时运行。

developer.mozilla.org/en-US/docs/…

原文链接:https://juejin.cn/post/7239256068743168058 作者:疾風亦有归途

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门