使用 Javascript 创建后端(6):NodeJS 内部如何运作?

NodeJS 的底层工作原理是什么?

在本节中,我们将进行一些理论研究,并了解 Nodejs 如何执行其 JavaScript 代码。

如你所知,NodeJS 允许执行异步代码。这个概念看似简单,但在后台却有点复杂。什么决定了执行什么代码?什么决定了执行顺序?

理解这些概念对于使用 NodeJS 进行开发至关重要。无需成为该主题的专家,但至少要了解基础知识。

请注意,为了更好地解释它们,一些概念已被简化。

NodeJS 的架构

NodeJS 由两个主要部分组成:V8 机器和 libuv

node-architecture.png

V8

负责将 JavaScript 代码转换为机器代码。一旦代码被转换为机器代码,执行将由 libuv 库管理

libuv

是一个开源库,用 C++ 编写,专门用于异步 i/o 执行(例如文件系统、网络等)

libuv 实现了 NodeJS 的两个非常重要的功能:事件循环和线程池

需要理解的一点是,NodeJS 在单线程模式下工作。

也就是说,它一次只能执行一个任务。如果一个任务需要太多的时间/资源,那么它将阻止/阻止其他任务运行。

想象一下,例如,如果网站上有 100,000 个用户同时请求访问数据库,响应时间很快就会变得不可接受。这就是为什么 NodeJS 需要高效管理异步代码执行的原因,这就是事件循环的工作

事件循环用于管理异步代码,例如回调、网络承诺和需要很少资源的请求。当任务执行时间过长时,为了不阻塞线程,事件循环会将这项工作委托给线程池。

线程池可以并行运行任务,因此可以处理更繁琐的任务,例如访问文件系统和非常苛刻的进程,例如视频转换或加密。

NodeJS 应用程序的执行顺序

运行 NodeJS 应用程序时,初始化代码、“需要”和顶层代码会立即一个接一个地执行。

我们代码中遇到的回调不会立即执行,因为可能会阻塞,它会阻止应用程序执行其他任务和其他用户。因此,这些回调已在事件循环中注册

一旦“顶层”代码执行完毕,NodeJS 就会将控制权交给事件循环,以便它可以执行其包含的任务。

事件循环根据预定义的标准决定必须遵守哪个执行顺序。事件循环还可以决定将真正漫长的任务委托给线程池。(例如访问文件系统)。

线程池可以同时执行多个任务(多线程),并将结果返回给事件循环

只要有任务要执行,事件循环就会保持应用程序处于活动状态。

一旦事件循环的所有任务都完成,控制权就会返回到应用程序的主线程,主线程将终止程序。

以 NodeJS 为例

理论很好,但这次让我们用一个具体的例子来回顾一下

1
2
3
4
5
6
7
8
9
10
const fs = require("fs");

console.log("First task started");

fs.readFile("./data/products.json", "utf8", (err, data) => {
console.log(data);
console.log("First task ended");
});

console.log("Second task started");

结果

1
2
3
4
5
6
7
8
9
First task started
Second task started
{
"name": "iPhone 12",
"price": 900
}


First task ended

根据前面解释的逻辑,NodeJS 将按以下顺序执行代码:

→ const fs = require (fs)

→ console.log(‘第一个任务已启动’)

→ 使用事件循环注册 readFile 回调

→ console.log(‘第二个任务已启动’)

→ 高级任务已完成,因此将手传递给事件循环

1
2
3
4
5
6
7
8
9
→ readFile callback → Delegate to the Thread Pool

→ When the readFile is finished

→ console.log(data)

→ console.log('First task ended')

→ If no other pending task then ends the Event Loop

→ 程序结束

setTimeout 示例

1
2
3
4
5
6
7
console.log("First");

setTimeout(() => {
console.log("Second");
}, 0);

console.log("Thrid");

结果

1
2
3
First
Third
Second

你会认为 setTimeOut0 时它会立即执行吗?但事实并非如此,如前所述,NodeJS 将回调发送到事件循环并首先执行顶层代码。

基于此逻辑,NodeJS 将按以下顺序执行代码:

→ console.log(‘First’)

→ 向事件循环注册 setTimeout 回调

→ console.log(‘Third’)

→ 移交给事件循环

1
2
3
4
5
→ callback setTimeout

→ console.log('Second')

→ If no other task then ends the Event Loop

→ 程序结束

服务器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require("http");

const server = http.createServer((req, res) => {
if (req.url === "/") {
res.end("<h1>Home page</h1>");
} else if (req.url === "/about") {
res.end("<h1>About page</h1>");

let i = 0;
do {
i++;
} while (i < 10000000000);
} else {
res.end("page not found");
}
});

server.listen(5000, "localhost", () => {
console.log("Server is listening at localhost on port 5000");
});

从这个例子中可以学到两个教训。首先,NodeJS 应用程序永远不会停止。事件循环是无止境的,因为它等待来自服务器的事件。listen 函数使事件循环保持活动状态。

最后,当用户访问关于页面时,Node 将执行 do while,由于它不是异步代码,因此所有用户对网站的访问将被暂时阻止,直到 do while 结束。这是一个很好的例子,说明 NodeJS 是单线程的,你必须小心编写应用程序。

例如,在这种情况下,最好将 do while 放在异步函数中,以免阻塞线程。


相关文章: