Node.js 如何在幕后工作?
Node.js 是基于 Google 的 V8 引擎的 JavaScript 运行时环境,即在 Node.js 的帮助下,我们可以在浏览器之外运行 JavaScript。关于 Node.js,您可能读过或未读过的其他内容是它是单线程的,基于事件驱动的架构,并且基于 I/O 模型是非阻塞的。
1) Node.js 架构: Node.js 由用 C++ 编写的Chrome V8 引擎和Libuv组成,Libuv 是一个多平台 C 库,支持基于事件循环和线程循环的异步 I/O 事件。不用担心,最后会在后面解释。我们需要记住的重要一点是,即使 Node.js 是使用用 C 或 C++ 编写的 V8 引擎和 Libuv 制作的,我们仍然可以在纯 JavaScript 中使用 Node.js。
2) Node.js 应用程序:既然我们已经了解了 Node.js 架构,是时候了解 Node.js 应用程序是如何运行的了,这部分包括 Node.js 单线程的概念及其非线程阻挡自然也是如此。那么,首先,什么是线程?简单来说,线程基本上是一组可以在计算机处理器中独立运行的编程指令,我们要运行的每个进程都有自己的线程来运行编程指令,并且该进程可以有多个线程。但是,要记住的一点是, Node.js 应用程序只能在单个线程上运行,也就是说,无论是 5 个用户还是 500 万用户使用 Node.js 应用程序,它都只会在单个线程上运行使 Node.js 应用程序可阻塞(这意味着一行代码可以阻塞整个应用程序,因为只使用了一个线程)。因此,为了保持 Node.js 应用程序运行,必须在任何具有回调函数的地方使用异步代码,因为我们知道异步代码会继续在后台运行,并且一旦 promise 得到解决,回调就会被执行,而不是同步代码阻塞整个应用程序,直到它完成执行。但是,我们仍然可以在应用程序的某个地方使用同步代码,并且那个地方是在我们的应用程序进入Event-loop之前。事件循环允许 Node.js 应用程序运行基于非阻塞异步 I/O 的操作,即所有异步代码都在事件循环中管理和执行,在此之前,我们可以使用我们的同步代码称为顶级代码的案例。因此,尝试只为那些在我们的应用程序启动时只执行一次而不是每次执行的操作编写同步代码,例如:从您的计算机内存中读取一些数据,稍后可以由某些用户(多次)请求异步代码。
下图是可视化的图表:
因此,正如您所看到的,每当 Node.js 应用程序在线程中启动时,第一步是初始化应用程序并执行顶级代码,正如我们之前所说,这是我们应该在应用程序中拥有的唯一同步代码.下一步是要求我们在代码中指定的模块(通常写在最顶部)。
下一步是注册我们代码中的所有事件回调,然后将其发送到事件循环以执行我们在节点应用程序中的大部分代码执行。但有时,有些任务太繁重,无法在我们的事件循环中由单个线程执行,因此这些任务被发送到线程池(由 Libuv 提供)是执行较重任务而不阻塞主线程的 4 个额外线程.线程数可以增加,用户不必指定必须卸载的任务,因为事件循环自己完成所有工作,但我们可以指定线程数。
3)事件循环:所以,你需要记住这一点,事件循环是我们所有异步代码执行的地方。有那么一会儿,你为什么不在第一段再读一遍,因为我们将介绍 Node.js 的第三点,即它基于事件驱动的架构。事件循环背后的整个想法是它适用于这种架构或这三个步骤,如下所示:
- 发出事件,这些事件可以从任何异步函数发出,例如获取 HTTP 请求、fileSystem 模块完成读取文件或计时器已完成。这些事件可能因我们的代码而异。
- 之后,事件循环将它们拾起。
- 执行回调函数(基于您的代码)。
除此之外,事件循环将较重的任务卸载到线程池。
注意:事件循环确实遵循运行回调的顺序。
通常,事件循环有 4 个阶段,并且对于每个阶段,它设置一个回调队列,其中包含来自已发出事件的回调函数。
- 第一阶段是Expired timeout callbacks ,它是来自 setTimeout()函数等的回调函数。
- 第二阶段是来自I/O 轮询的回调,例如读取文件或任何 HTTP 请求的事件。
- 第三阶段是来自setImmediate()函数的回调,这是用户想要在 I/O 轮询之后执行的回调函数。这些类型的功能可能只针对某些特定情况。
- 最后一个是关闭回调,这些回调是从关闭网络服务器等事件发出的。
注意:从某个阶段可用的所有回调首先执行,然后才进入下一个阶段。此外,在事件循环结束时,它检查是否有任何其他事件仍在进行,如果是,则返回第一阶段,依此类推,否则程序退出事件循环。