【一】理解Node.js事件循环

2024-03-04  本文已影响0人  涅槃快乐是金

你已经使用 Node.js一段时间了。你已经构建了一些应用程序,尝试了不同的模块,甚至对异步编程感到很舒适。但有件事一直在困扰着你 —— 事件循环

如果你像我一样,你已经花了无数个小时阅读文档和观看视频,试图理解事件循环。但即使作为一名经验丰富的开发者,要完全理解它的工作原理也很困难。

JavaScript 中的异步编程

我们将从 JavaScript 中的异步编程开始。尽管 JavaScriptweb、移动和桌面应用程序中被广泛使用,但重要的是要记住,JavaScript 在其最基本的形式中是一种同步、阻塞、单线程的语言。让我们用一小段代码来理解这句话。

// index.js

function A() {
  console.log("A");
}

function B() {
  console.log("B");
}

A()
B()

// Logs A and then B
JavaScript 是同步的

如果我们有两个函数将消息记录到控制台,代码将自上而下执行,任何时候只有一行在执行。在代码片段中,我们看到AB之前被执行。

JavaScript 是阻塞的

JavaScript是阻塞的,因为它是同步的。无论前一个过程花费多长时间,后续的过程都不会启动,直到前一个过程完成。在代码片段中,如果函数A 必须执行一大块密集的代码,JavaScript 必须在执行完毕之前完成它,而不会转到函数B。即使那段代码需要花费 10 秒或 1 分钟的时间。

你可能在浏览器中经历过这种情况。当 web应用程序在浏览器中运行并且执行了一大块密集的代码而没有将控制返回给浏览器时,浏览器可能会出现冻结的情况。这就是所谓的阻塞。浏览器被阻止继续处理用户输入和执行其他任务,直到 web应用程序将处理器的控制权返回。

JavaScript 是单线程的

线程就是你的 JavaScript程序可以用来运行任务的过程。每个线程一次只能做一件事情。不像一些其他支持多线程的语言可以同时运行多个任务,JavaScript 只有一个线程,称为主线程,用于执行任何代码。

等待 JavaScript

正如你可能猜到的那样,这种JavaScript模型会产生问题,因为我们必须等待数据被获取,然后才能继续执行代码。这个等待可能需要几秒钟,在此期间我们无法运行任何进一步的代码。如果 JavaScript 在等待过程中继续执行,我们将会遇到错误。我们需要一种方法来在JavaScript中实现异步行为。这时候就引入了 Node.js

Node.js 运行时

Node.js运行时是一个环境,您可以在其中在浏览器之外使用和运行JavaScript程序。在其核心,Node运行时由三个主要组件组成。

虽然所有部分都很重要,但 Node.js 中异步编程的关键组件是外部依赖项libuv

Libuv

Libuv 是一个跨平台的开源库,用C 语言编写。在Node.js 运行时中,其角色是提供支持处理异步操作。让我们来看看它是如何工作的。

Node.js 运行时中的代码执行

图片显示左侧代表 V8引擎的矩形块,右侧代表libuv的矩形块
让我们来概括一下代码在 Node运行时中的典型执行方式。当我们执行代码时,位于图像左侧的 V8引擎负责执行 JavaScript代码。该引擎包括内存堆和调用堆栈。

无论我们声明变量还是函数,都会在堆上分配内存,无论何时执行代码,函数都会被推送到调用堆栈中。当函数返回时,它就会从调用堆栈中弹出。这是堆栈数据结构的一个简单实现,其中最后添加的项目是第一个被移除的。在图像右侧,我们有libuv,它负责处理异步方法。

每当我们执行一个异步方法时,libuv接管任务的执行。然后,libuv使用操作系统的本地异步机制来运行任务。如果本地机制不可用或不足,它会利用线程池来运行任务,以确保主线程不被阻塞。

同步代码执行

首先,让我们来看看同步代码执行。以下代码包含三个控制台日志语句,分别记录 "First"、"Second" 和 "Third"。让我们像运行时正在执行一样来看代码。

// index.js
console.log("First");
console.log("Second");
console.log("Third");

以下是使用 Node 运行时可视化同步代码执行的方式。


执行的主线程总是从全局范围开始。全局函数,如果我们可以这样称呼它,被推送到堆栈上。然后,在第 1 行,我们有一个控制台日志语句。函数被推送到堆栈上。假设这发生在 1 毫秒时,"First"被记录到控制台。然后,函数从堆栈中弹出。

执行到达第 3 行。假设在 2 毫秒时,日志函数再次被推送到堆栈上。"Second" 被记录到控制台,然后函数从堆栈中弹出。

最后,执行到达第 5 行。在 3 毫秒时,函数被推送到堆栈上,"Third"被记录到控制台,然后函数从堆栈中弹出。没有更多的代码要执行,全局范围也被弹出。

异步代码执行

接下来,让我们看一下异步代码的执行。考虑下面的代码片段。有三个日志语句,但这次第二个日志语句位于传递给 fs.readFile()的回调函数中。

执行的主线程总是从全局范围开始。全局函数被推送到堆栈上。然后,执行来到第 1 行。在 1 毫秒时,"First"在控制台中被记录,然后函数从堆栈中弹出。然后,执行继续到第 3 行。在 2 毫秒时,readFile 方法被推送到堆栈上。由于readFile是一个异步操作,它被转移到 libuv。

JavaScript 从调用堆栈中弹出 readFile 方法,因为就第 3 行的执行而言,它的任务已经完成。在后台,libuv 开始在单独的线程上读取文件内容。在 3 毫秒时,JavaScript 继续到第 7 行,将log函数推送到堆栈上,"Third" 被记录到控制台,然后函数从堆栈中弹出。

大约在 4 毫秒时,假设文件读取任务在线程池中完成。现在,相关的回调函数在调用堆栈上执行。在回调函数中,遇到了log语句。

它被推送到调用堆栈上,"Second" 被记录到控制台,然后log 函数被弹出。由于在回调函数中没有更多的语句要执行,回调函数也被弹出。没有更多的代码可运行,所以全局函数也被从堆栈中弹出。

控制台输出将会是"First""Third",然后是 "Second"

Libuv 和异步操作

很明显,libuvNode.js 中帮助处理异步操作。对于像处理网络请求这样的异步操作,libuv 依赖于操作系统的原语。对于像读取没有本地操作系统支持的文件这样的异步操作,libuv依赖于其线程池,以确保主线程不被阻塞。然而,这确实引发了一些问题。

什么是事件循环?

从技术上讲,事件循环只是一个 C程序。但是,您可以将其视为一种设计模式,用于协调 Node.js 中同步和异步代码的执行。

事件循环视图展示

事件循环是一个循环,只要您的 Node.js应用程序正在运行,它就会持续运行。每个循环中有六个不同的队列,每个队列最终都会保存一个或多个需要在调用堆栈上执行的回调函数。

事件循环由 6 个不同队列组成。

最后,有微任务队列,其中包含两个单独的队列。

需要注意的是,定时器、I/O、检查和关闭队列都是 libuv 的一部分。然而,两个微任务队列不是libuv 的一部分。尽管如此,它们仍然是 Node 运行时的一部分,并且在回调执行顺序中起着重要作用。说到这一点,让我们继续理解下一步。

事件循环的工作原理

箭头已经给了提示,但很容易让人感到困惑。让我解释一下队列的优先顺序。首先,要知道所有用户编写的同步JavaScript代码优先于运行时希望执行的异步代码。这意味着只有在调用堆栈为空时,事件循环才会发挥作用。

如果在此时还有更多的回调需要处理,循环将被保持活动状态进行另一次运行,并重复相同的步骤。另一方面,如果所有回调都已执行,并且没有更多的代码需要处理,则事件循环退出。

这就是libuv的事件循环在 Node.js中执行异步代码中所扮演的角色。有了这些规则,我们可以重新审视之前的问题。

我们学到了更多,但下面这个可视化表示(与上面相同)是我希望你将其牢记在心的,因为它展示了 Node.js 在幕后执行异步代码的方式。


“但等等,验证这个可视化的代码在哪里?”你可能会问。好吧,事件循环中的每个队列在执行上都有细微差别,因此最好一次处理一个。

结论

事件循环是 Node.js 的基本部分,通过确保主线程不被阻塞来实现异步编程。理解事件循环的工作原理可能会有挑战,但对于构建性能优异的应用程序至关重要。

文章涵盖了 JavaScript 中异步编程的基础知识、Node.js运行时以及负责处理异步操作的libuv。有了这些知识,您可以构建事件循环的强大思维模型,这将帮助您编写利用 Node.js 异步特性的代码。

上一篇下一篇

猜你喜欢

热点阅读