NodeJS 的底层工作原理是什么?
在本节中,我们将进行一些理论研究,并了解 Nodejs
如何执行其 JavaScript
代码。
如你所知,NodeJS
允许执行异步代码。这个概念看似简单,但在后台却有点复杂。什么决定了执行什么代码?什么决定了执行顺序?
理解这些概念对于使用 NodeJS
进行开发至关重要。无需成为该主题的专家,但至少要了解基础知识。
请注意,为了更好地解释它们,一些概念已被简化。
NodeJS 的架构
NodeJS
由两个主要部分组成:V8
机器和 libuv
库
V8
负责将 JavaScript
代码转换为机器代码。一旦代码被转换为机器代码,执行将由 libuv
库管理
libuv
是一个开源库,用 C++
编写,专门用于异步 i/o
执行(例如文件系统、网络等)
libuv
实现了 NodeJS
的两个非常重要的功能:事件循环和线程池
需要理解的一点是,NodeJS
在单线程模式下工作。
也就是说,它一次只能执行一个任务。如果一个任务需要太多的时间/资源,那么它将阻止/阻止其他任务运行。
想象一下,例如,如果网站上有 100,000 个用户同时请求访问数据库,响应时间很快就会变得不可接受。这就是为什么 NodeJS
需要高效管理异步代码执行的原因,这就是事件循环的工作
事件循环用于管理异步代码,例如回调、网络承诺和需要很少资源的请求。当任务执行时间过长时,为了不阻塞线程,事件循环会将这项工作委托给线程池。
线程池可以并行运行任务,因此可以处理更繁琐的任务,例如访问文件系统和非常苛刻的进程,例如视频转换或加密。
NodeJS 应用程序的执行顺序
运行 NodeJS
应用程序时,初始化代码、“需要”和顶层代码会立即一个接一个地执行。
我们代码中遇到的回调不会立即执行,因为可能会阻塞,它会阻止应用程序执行其他任务和其他用户。因此,这些回调已在事件循环中注册
一旦“顶层”代码执行完毕,NodeJS
就会将控制权交给事件循环,以便它可以执行其包含的任务。
事件循环根据预定义的标准决定必须遵守哪个执行顺序。事件循环还可以决定将真正漫长的任务委托给线程池。(例如访问文件系统)。
线程池可以同时执行多个任务(多线程),并将结果返回给事件循环
只要有任务要执行,事件循环就会保持应用程序处于活动状态。
一旦事件循环的所有任务都完成,控制权就会返回到应用程序的主线程,主线程将终止程序。
以 NodeJS 为例
理论很好,但这次让我们用一个具体的例子来回顾一下
1 | const fs = require("fs"); |
结果
1 | First task started |
根据前面解释的逻辑,NodeJS
将按以下顺序执行代码:
→ const fs = require (fs)
→ console.log(‘第一个任务已启动’)
→ 使用事件循环注册 readFile 回调
→ console.log(‘第二个任务已启动’)
→ 高级任务已完成,因此将手传递给事件循环
1 | → readFile callback → Delegate to the Thread Pool |
→ 程序结束
setTimeout 示例
1 | console.log("First"); |
结果
1 | First |
你会认为 setTimeOut
为 0
时它会立即执行吗?但事实并非如此,如前所述,NodeJS
将回调发送到事件循环并首先执行顶层代码。
基于此逻辑,NodeJS
将按以下顺序执行代码:
→ console.log(‘First’)
→ 向事件循环注册 setTimeout 回调
→ console.log(‘Third’)
→ 移交给事件循环
1 | → callback setTimeout |
→ 程序结束
服务器示例
1 | const http = require("http"); |
从这个例子中可以学到两个教训。首先,NodeJS
应用程序永远不会停止。事件循环是无止境的,因为它等待来自服务器的事件。listen
函数使事件循环保持活动状态。
最后,当用户访问关于页面时,Node
将执行 do while
,由于它不是异步代码,因此所有用户对网站的访问将被暂时阻止,直到 do while
结束。这是一个很好的例子,说明 NodeJS
是单线程的,你必须小心编写应用程序。
例如,在这种情况下,最好将 do while
放在异步函数中,以免阻塞线程。
相关文章: