Nodejs组成和原理笔记 01 组成和原理
前言
一直想了解nodejs的基本原理,但苦于首先确实c/c 不是很了解,其次本身主攻的也是前端,没有时间和精力去精进这块,长期处于一个主要nodejs模块原理都懂,但更进一步十分困难的底部, 后面发现国内一位nodejs大神theanarkh已经写了非常详细的Nodejs底层原理的电子书.
惊喜之余,发现有些描述自己可能不是特别理解,再多方查阅资料的基础上,再次把其语言白话一些,案例从c/c 代码被我映射为javascript代码,作为笔记,跟大家一起学习。
Nodejs组成和原理
Node.js简单架构
将Node.js分成三层
-
首先最上层是就是我们写代码require的模块,也就是标准库,如Http, Fs 模块。
-
中间层
node bindings
主要是使 js 和 C/C 进行通信- Node.js的一个重要特性是它的扩展能力,可以通过C 编写插件来扩展Node.js的功能。
- Node.js的Node API提供了一组C 类和函数,可以让开发者使用C 来编写Node.js扩展。这些扩展可以被称为“Node binding”,也被称为“native addons”。
- 当然它也可以调用底层,比如V8和libuv提供的接口,从而访问v8和libuv
-
最下面这一层是支撑 nodejs 运行的关键,主要由
v8
、libuv
、c-ares
等模块组成,向上一层提供 api 服务- V8是javacript的runtime,解析javascript代码的
- libuv主要是提供跨平台的I/O能力的
- http_parserL、zlib 等:提供包括 http 解析、数据压缩等能力
V8和libuv如何通信
上面可以看到,我们js代码首先是被v8引擎解析的,然后才走到nodejs bindings层,也就是到这里才真正的开始调用底层的C 和C提供能给js的功能。
关键点是简单介绍一下libuv
在 Node.js 中,libuv 是一个跨平台的异步 I/O 库,负责实现事件循环机制。事件循环是 Node.js 的核心,它负责管理所有的异步操作,并且在它们完成时通知应用程序。事件循环可以理解为一个无限循环,每次循环处理一个事件。
libuv 提供了对底层操作系统的访问,包括文件 I/O、网络 I/O、定时器、进程管理等等,它能够利用操作系统提供的异步 I/O 机制来提高 I/O 操作的效率,从而使 Node.js 能够高效地处理大量并发请求。
libuv如何跟其他模块交互
也就是说libuv调用了http,zlib等底层模块去处理请求,libuv相当于一个调度器,调度器模块很常见,比如k8s的调度器(比如调度某个请求到底去哪个pod),rxjs的调度器(对于某个操作符改变其默认调度行为),react的调度器(实现比如任务优先级处理,中断和恢复fiber生成处理等)
例如,在处理 HTTP 请求时,Node.js 应用程序会使用 http 模块来创建一个 HTTP 服务器,当收到客户端请求时,http 模块会调用 libuv 实现的网络 I/O 接口来处理网络请求,然后将请求传递给应用程序。
如何在V8新增一个自定义的功能?
// C 里定义
Handle<FunctionTemplate> Test = FunctionTemplate::New(cb);
global->Set(String::New(“Test"), Test);
// JS里使用
const test = new Test();
我们解释下上面的代码:
这段代码在 V8 中自定义了一个功能,供 JavaScript 调用,通过如下步骤实现:
- 定义:在 C 代码中,使用 FunctionTemplate::New(cb) 定义一个 FunctionTemplate 类型的变量 Test,其中 cb 是回调函数,它将在 JavaScript 调用该函数时被执行。
- 关联:使用 global->Set(String::New("Test"), Test) 将变量 Test 关联到键为 "Test" 的值。
- 使用:在 JavaScript 代码中,通过使用语句 const test = new Test() 创建一个新的对象,该对象是由定义的 FunctionTemplate 创建的,因此它具有所有 FunctionTemplate 所指定的功能。
通过以上步骤,可以在 JavaScript 代码中访问在 C 代码中定义的函数并调用它,从而与 C 代码进行交互。 FunctionTemplate::New(cb)相当于常见了一个Test类,用的FunctionTemplate模板,具体实现我们不得而知。
你看了之后应该会很懵逼,没有关系,我也不会c ,遇到不会的语法,查一下就行了, 如果你不懂,你可能没有明白为什么c 还能实现javascript的语法功能?这两个不是不同的语言吗?
我简单说明一下
在V8引擎里,javascript跟c 的关系
首先,你要明白,javascript最终传给编译器的就是字符串,这个字符串你解析之后,如果能实现javascript这门语言要求的语法规则、词法规则等等,那么我就认为你就实现了javascript的运行时(runtime)。
假如我们用go,我们rust等语言,我们能解析这段字符串,并且把这段字符串表达出的整个运行过程和结果满足javascript这门语言所要求的的一切,那么我们也就是实现了javascript的运行时
V8引擎是用C 编写的,它将JavaScript代码解析为抽象语法树(AST),然后通过执行相应的C 代码来实现JavaScript代码的运行。因此,JavaScript代码本质上是在V8引擎中通过C 代码实现的。
所以说你不要奇怪,为啥c 跟javascript明明是不同的语言为什么js还能调用c 的代码。
简单总结为,在V8引擎中,要新增一个自定义的功能,通常需要编写一些C 代码来实现。C 代码可以使用V8提供的API来访问JavaScript对象、函数、属性等。同时,也可以使用C 来访问操作系统的底层API,从而实现一些高级的功能。
将C 代码编译为动态链接库:一旦编写好了C 代码,需要将其编译为动态链接库,以便在JavaScript中调用。
一旦加载了动态链接库,就可以在JavaScript中调用C 函数了。通常需要使用Node.js的N-API或nan库来编写绑定代码,将C 函数绑定到JavaScript对象上,并提供相应的接口。
libuv中的事件循环详解
我们结合源码一起梳理一下
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
/* 这是一个while循环 */
while (r != 0 && loop->stop_flag == 0) {
/* 更新时间并开始倒计时, 对应上图的update loop time阶段*/
uv__update_time(loop);
/* 对应上面的run du timers阶段 */
uv__run_timers(loop);
/* 处理挂起的handle, 对应上面的 call pending callbacks阶段 */
ran_pending = uv__run_pending(loop);
/* 运行idle handle, 对应上面的 run idle callbacks阶段 */
uv__run_idle(loop);
/* 运行prepare handle, 对应上面 run prepare handles阶段 */
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
/* 计算要阻塞的时间,开始阻塞, 对应上面的 poll阶段 */
uv__io_poll(loop, timeout);
/* 程序执行到这里表示被唤醒了,被唤醒的原因可能是I/O可读可写、或者超时了,检查handle是否可以操作, 对应 check阶段 */
uv__run_check(loop);
/* 看看是否有close的handle, 对应close阶段 */
uv__run_closing_handles(loop);
/* 单次模式 */
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have
* been invoked when it returns. uv__io_poll() can return without doing
* I/O (meaning: no callbacks) when its timeout expires - which means we
* have pending timers that satisfy the forward progress constraint.
*
* UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
* the check.
*/
uv__update_time(loop);
uv__run_timers(loop);
}
/* handle保活处理 */
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
/* The if statement lets gcc compile it to a conditional store. Avoids
* dirtying a cache line.
*/
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
Update loop time
- 描述
- libuv获取当前的时间,并在本轮事件循环的中使用这个时间(除非在必要的时候更新这个时间)
- 优点
- 目的是获取时间是一个系统调用的操作,减少过多系统调用的性能损耗
- 缺点
- 时间不那么精确
Loop alive
- 描述
loop alive
是这一阶段(event loop)保持活动状态的一个重要部分,当为false的时候退出这个事件循环。
run due timers
-
描述
run due timers
是指执行到期的定时器回调函数的过程。定时器是Node.js中常用的异步机制,可以让您在指定的时间间隔后执行代码
-
优点:
- 灵活:定时器提供了多种不同的选项,如间隔时间、重复次数、以及是否在其他事件处理完成后立即执行等,因此非常灵活。
-
缺点:
- 精度问题:由于Node.js的事件循环是单线程的,因此定时器的精度可能受到影响。如果事件队列中有大量的任务,定时器的回调函数可能会延迟较长的时间。
- 注意事项:如果没有正确处理定时器,可能会造成内存泄漏。因此,在使用定时器时需要仔细考虑其使用情况,并确保能够及时清除不再需要的定时器。
Call pending callbacks
对应调用上面的uv__run_pending()函数,主要是用来执行之前加入到pending queue里的函数。
在 Node.js 中,pending queue(待处理队列)是一个存储待处理事件回调函数的队列。当某些事件的回调函数被添加到事件队列中,但是由于当前事件循环阶段正在执行,导致这些回调函数不能立即执行时,它们就会被添加到 pending queue 中等待处理。
Node.js 中有很多异步 API,例如 setTimeout()、setInterval()、setImmediate()、process.nextTick() 等,它们都可以用来将回调函数添加到事件队列中。如果在当前事件循环阶段结束之前,这些回调函数无法执行,它们就会被添加到 pending queue 中等待下一个事件循环执行。这些被添加到 pending queue 中的回调函数,通常被称为 pending callbacks(待处理回调函数)。
为什么这里会把setTimeout加入进来,不是setTimeout的执行在timers阶段吗?
如果在执行 timers 阶段时,发现有一个计时器的时间已经到了,那么对应的回调函数就会被立即执行,而不会被添加到 pending queue 中等待下一个事件循环执行。但如果在执行 timers 阶段时,所有的计时器事件都还没有到期,那么 setTimeout() 函数添加的回调函数就会被添加到事件队列的末尾,等待下一次事件循环执行
Run idle handles
- 描述
- "Run idle handles" 指的是执行已经注册在 Node.js 系统中的任何 "空闲" 任务的过程。这些任务旨在在系统空闲时执行,并且没有其他要做的工作。
大家肯定会有疑问,"空闲"任务是什么,比如说:
- 清理未使用的资源:释放内存,关闭连接或释放任何不再需要的资源。
- 执行后台计算:需要大量处理能力的任务,但可以在后台执行,不影响系统其他部分的性能。
- 执行例行维护:如检查磁盘空间或其他例行系统检查的任务。
所以这个基本跟我们使用者没啥关系。
- 优点
- 它们允许后台任务在不影响系统其他部分性能的情况下有效地执行。这对于诸如清理资源、检查更新或执行任何其他低优先级工作的任务是有用的
- 缺点
- 使用空闲句柄也有一些潜在的缺点。如果不小心实现,空闲任务可以消耗大量的 CPU 和内存,从而影响系统其他部分的性能
Run prepare handles
在Idle阶段完成之后,事件循环会进入到Poll阶段。在Poll阶段中,Node.js会等待I/O事件发生。在等待的过程中,如果prepare阶段添加了回调函数,那么这些回调函数会被执行。执行完prepare回调函数之后,事件循环会检查是否有I/O事件发生。如果有,就会执行相应的I/O回调函数。如果没有,就会进入到Check阶段。
Poll for I/O
描述
- 该阶段,如果事件队列中有回调函数,那么会立即执行它们,直到队列为空或者执行了一定数量的回调函数。
- 如果事件队列中没有回调函数,那么 Node.js 将会等待新的事件加入到事件队列中。在这个阶段,Node.js 会暂停 JavaScript 执行环境,等待 I/O 事件完成。例如,如果应用程序正在等待从网络或磁盘读取数据,那么 Node.js 将会等待这些操作完成,并将读取的数据传递给应用程序。
- 当 I/O 事件完成时,Node.js 会将事件的回调函数添加到事件队列中,等待下一次 Poll 阶段执行。
这个阶段跟Call pending callbacks有什么区别呢?
-
在执行 Poll 阶段时,Node.js 会等待 I/O 事件完成,并执行相应的回调函数。如果事件队列中没有回调函数等待执行,那么 Node.js 将会在这个阶段等待新的事件加入到事件队列中。因此,Poll 阶段通常是用来处理 I/O 事件的,例如网络请求、文件读取等。
-
而在执行 Call pending callbacks 阶段时,Node.js 会执行之前被延迟执行的回调函数,这些回调函数可能来自其他阶段,例如 timers、I/O callbacks 或者其他自定义阶段。这些回调函数通常是在当前事件循环中被添加到事件队列中,但是因为前面的阶段还没有执行完毕,导致它们没有得到及时执行。因此,Call pending callbacks 阶段通常是用来处理之前被延迟执行的回调函数。
Run check handles
- 描述
- 它的作用是检查是否有需要立即执行的任务,例如,setImmediate() 回调函数。
- 有些任务是需要加入到check队列的,这些任务就是在这个阶段
Call close callbacks
-
描述
-
在 Node.js 中,Call close callbacks 阶段是事件循环的最后一个阶段,它的作用是处理一些与关闭事件相关的回调函数。这些回调函数通常与网络连接或文件描述符相关,包括 close、error、end 等事件的回调函数。
-
在执行这个阶段时,Node.js 会按照添加顺序,依次调用所有添加到 close callback 队列中的回调函数。这个阶段的执行时机是在每个事件循环的最后,当事件队列中的所有事件处理完毕之后才会执行。因此,如果在事件循环过程中有某些资源(如网络连接或文件描述符)被关闭了,它们对应的 close callback 回调函数会被添加到 close callback 队列中,在下一个事件循环的 Call close callbacks 阶段得到执行。
-
需要注意的是,Node.js 并不保证 close callback 队列中的回调函数一定会在当前事件循环中被执行。如果在执行该阶段时,还有一些其它阶段的回调函数没有得到执行,那么 close callback 回调函数就会被推迟到下一个事件循环的 Call close callbacks 阶段再执行。
-
下面我能通过一个例子来了解libuv的基本原理。
// libuv库导出
import { uv_idle_stop, uv_idle_t } from 'uv'
// 定义了一个全局变量 counter,用于记录事件循环的循环次数。
let counter = 0;
// 该函数会在事件循环的 idle 阶段被调用
function wait_for_a_while() {
counter ;
if(counter >= 10e6) {
// 停止 idle 阶段的事件循环,即结束 wait_for_a_while 函数的执行
uv_idle_stop(handle);
}
}
function main(){
var idler = new uv_idle_t;
// 获取事件循环的核心结构体。并初始化一个idle
uv_idle_init(uv_default_loop(), idler);
// 往事件循环的idle阶段插入一个任务
uv_idle_start(idler, wait_for_a_while);
// 启动事件循环
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
// 销毁libuv的相关数据
uv_loop_close(uv_default_loop());
}
然后执行main(), 事件循环就启动了。
在原文中,作者把每个函数比如uv_idle_init列出来源码,但是我觉得只要了解其主要作用就行了,这部分源码没有什么必要细究。
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanggiahh
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
怎样阻止微信小程序自动打开
PHP中文网 06-13 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01