事件循环在 JavaScript 中是如何工作的?

虽然可能需要深入了解 C++ 和 C 等语言才能编写全面的生产代码,但通常只需基本了解该语言可以做什么就可以编写 JavaScript。

将回调传递给函数或编写异步代码等概念通常并不难实现,这使得大多数 JavaScript 开发人员不太关心幕后发生的事情。 他们只是不关心理解语言已经从他们身上深深抽象出来的复杂性。

作为一名 JavaScript 开发人员,了解幕后真正发生的事情以及从我们身上抽象出来的这些复杂性中的大部分是如何真正起作用的变得越来越重要。 它帮助我们做出更明智的决定,这反过来又可以大大提高我们的代码性能。

本文重点介绍 JavaScript 中一个非常重要但很少被理解的概念或术语。 事件循环!

写异步代码在 JavaScript 中是避免不了的,但是为什么一个代码异步运行真的意味着什么呢? 即事件循环

在我们了解事件循环是如何工作之前,我们首先必须了解 JavaScript 本身是什么以及它是如何工作的!

什么是JavaScript?

在我们继续之前,我希望我们先回到最基本的部分。 JavaScript 到底是什么? 我们可以将 JavaScript 定义为;

JavaScript 是一种高级、解释型、单线程、非阻塞、异步、并发的语言。

等等,这是什么? 书本化的定义? 🤔

让我们分解一下!

本文的关键词是单线程、非阻塞、并发和异步。

单线程

执行线程是可以由调度程序独立管理的最小的编程指令序列。 编程语言是单线程的,这意味着它一次只能执行一个任务或操作。 这意味着它将从头到尾执行整个过程,而不会中断或停止线程。

与多线程语言不同,在多线程语言中,多个进程可以同时在多个线程上运行而不会相互阻塞。

JavaScript 如何同时是单线程和非阻塞的?

但是阻塞是什么意思?

非阻塞

阻塞没有唯一的定义; 它只是简单地表示线程上运行缓慢的事物。 所以非阻塞意味着线程上的事情并不慢。

但是等等,我说过 JavaScript 在单线程上运行吗? 而且我还说它是非阻塞的,这意味着任务在调用堆栈上运行得很快? 但是怎么办??? 当我们运行计时器时怎么样? 循环?

放松! 我们稍后会发现 😉。

同时

并发意味着代码被多个线程同时执行。

好的,事情变得很真实 诡异的 现在,JavaScript 怎么能同时是单线程和并发的呢? 即,用多个线程执行它的代码?

异步

异步编程意味着代码在事件循环中运行。 当有阻塞操作时,启动事件。 阻塞代码保持运行而不阻塞主执行线程。 当阻塞代码完成运行时,它将阻塞操作的结果排队并将它们推回堆栈。

  使用这 6 个在线语言标识符了解语言学

但是 JavaScript 有单线程吗? 那么什么在让线程中的其他代码得到执行的同时执行这个阻塞代码?

在我们继续之前,让我们回顾一下上面的内容。

  • JavaScript 是单线程的
  • JavaScript 是非阻塞的,即慢进程不会阻塞它的执行
  • JavaScript 是并发的,即它同时在多个线程中执行其代码
  • JavaScript 是异步的,即它在其他地方运行阻塞代码。

但是上面这些不完全相加,单线程语言怎么可能是非阻塞的、并发的、异步的呢?

让我们更深入一点,让我们深入到 JavaScript 运行时引擎,V8,也许它有一些我们不知道的隐藏线程。

V8引擎

V8 引擎是一种高性能的开源 Web 程序集运行时引擎,用于 JavaScript,由 Google 用 C++ 编写。 大多数浏览器使用 V8 引擎运行 JavaScript,甚至流行的 node js 运行环境也使用它。

用简单的英语来说,V8 是一个 C++ 程序,它接收 JavaScript 代码,编译并执行它。

V8 做了两件主要的事情;

  • 堆内存分配
  • 调用堆栈执行上下文

可悲的是,我们的怀疑是错误的。 V8 只有一个调用栈,把调用栈想象成线程。

一个线程 === 一个调用堆栈 === 一次执行一次。

图片——黑客中午

既然 V8 只有一个调用栈,那么 JavaScript 如何在不阻塞主执行线程的情况下并发异步运行呢?

让我们通过编写一个简单但常见的异步代码来尝试找出并一起分析它。

JavaScript 逐行运行每个代码,一个接一个(单线程)。 正如预期的那样,第一行在这里打印在控制台中,但为什么最后一行打印在超时代码之前? 为什么执行过程在继续运行最后一行之前不等待超时代码(阻塞)?

其他一些线程似乎帮助我们执行了该超时,因为我们非常确定一个线程在任何时间点只能执行一个任务。

让我们先睹为快 V8 源代码 一阵子。

等等……什么??!!! V8里面没有定时器功能,没有DOM? 没有活动? 没有 AJAX 吗?… 是的!

事件、DOM、定时器等不是 JavaScript 核心实现的一部分,JavaScript 严格遵守 Ecma Scripts 规范,其各种版本通常根据其 Ecma Scripts Specifications (ES X) 进行引用。

执行工作流程

事件、计时器、Ajax 请求均由浏览器在客户端提供,通常称为 Web API。 它们让单线程 JavaScript 成为非阻塞、并发和异步的! 但是怎么办?

  什么是沙盒环境?

任何 JavaScript 程序的执行工作流都包含三个主要部分:调用堆栈、Web API 和任务队列。

调用堆栈

堆栈是一种数据结构,其中最后添加的元素总是最先从堆栈中移除,您可以将其视为一个盘子的堆栈,其中只有最后添加的第一个盘子可以首先移除。 调用堆栈只不过是一个堆栈数据结构,其中任务或代码正在相应地执行。

让我们考虑以下示例;

来源 – https://youtu.be/8aGhZQkoFbQ

当您调用函数 printSquare() 时,它被压入调用堆栈,printSquare() 函数调用 square() 函数。 square() 函数被压入堆栈并调用 multiply() 函数。 乘法函数被压入堆栈。 由于 multiply 函数返回并且是最后被压入堆栈的东西,它首先被解析并从堆栈中删除,然后是 square() 函数,然后是 printSquare() 函数。

网络 API

这是执行 V8 引擎未处理的代码的地方,不会“阻塞”主执行线程。 当 Call Stack 遇到 Web API 函数时,立即将流程交给 Web API 执行,并在执行期间释放 Call Stack 以执行其他操作。

让我们回到上面的 setTimeout 示例;

当我们运行代码时,第一个 console.log 行被压入堆栈,我们几乎立即得到输出,在到达超时时,计时器由浏览器处理并且不是 V8 核心实现的一部分,它被压入改为 Web API,释放堆栈以便它可以执行其他操作。

当超时仍在运行时,堆栈继续执行下一行操作并运行最后一个 console.log,这解释了为什么我们在计时器输出之前输出它。 一旦计时器完成,就会发生一些事情。 然后计时器中的 console.log 再次神奇地出现在调用堆栈中!

如何?

事件循环

在我们讨论事件循环之前,让我们先了解一下任务队列的功能。

回到我们的超时示例,一旦 Web API 完成任务执行,它不会自动将其推回调用堆栈。 它进入任务队列。

队列是一种遵循先进先出原则的数据结构,因此当任务被推入队列时,它们会以相同的顺序退出。 由 Web API 执行的任务被推送到任务队列,然后返回到调用堆栈以打印出它们的结果。

可是等等。 事件循环到底是什么???

来源 – https://youtu.be/8aGhZQkoFbQ

事件循环是在将回调从任务队列推送到调用堆栈之前等待调用堆栈被清除的过程。 一旦 Stack 清空,事件循环就会触发并检查 Task Queue 是否有可用的回调。 如果有,则将其压入Call Stack,等待Call Stack再次清空,重复同样的过程。

  如何加快您的互联网连接

资料来源 – https://www.quora.com/How-does-an-event-loop-work/answer/Timothy-Maxwell

上图演示了事件循环和任务队列之间的基本工作流程。

结论

虽然这是一个非常基本的介绍,但 JavaScript 中异步编程的概念提供了足够的洞察力,可以清楚地了解幕后发生的事情以及 JavaScript 如何能够仅通过单个线程并发和异步运行。

JavaScript 总是按需提供的,如果你有兴趣学习,我建议你看看这个 Udemy课程.