Node.js的原理分析
希望能对Node有一定基础的了解,方便以后深入与使用。从理解四个方面来一步步分析研究,Node的分析主要包括如下几部分:
一、Node.js是什么?
二、什么是V8?
三、Node.js的技术架构
四、Node.js的工作流程
一、Node.js是什么?
1、Node.js是什么
Node.js 是一个开源、跨平台的 JavaScript 运行时环境。
2、Node.js的特点
-
在浏览器外运行 V8 JavaScript 引擎。
-
运行于单进程,原生异步I/O,不阻塞。
-
基于npm有大量的库,上层框架成熟。
3、Node.js的优势
处理高并发、I/O密集的场景优势明显。
nodejs单线程指的是主线程,IO操作是系统底层多线程调度。
CPU密集与I/O密集:
-
CPU密集:计算、加密解密、压缩、图像处理等。
-
I/O密集 : 文件操作、网络操作、数据库操作等。
二、什么是V8?
1、先看看V8的简要理解。
V8 是一个 JavaScript 引擎的名称。
-
C++编写
-
负责处理并执行 JavaScript。
-
现阶段执行js最快的一个引擎。
-
同时也是js在服务器端运行提供支持的引擎。
2、V8有实现了哪一些功能?
- 将JS源代码变成本地代码并执行
- 维护调用栈,确保JS函数的执行顺序
- 内存管理,为所有对象分配内存
- 垃圾回收
- 实现JS的标准库
三、Node.js的技术架构
一句话描述:Node.js 内置的fs、http等核心模块通过 C++ Bindings 调用 libuv、c-ares、llhttp 等 C/C++类库,从而接入操作系统提供的平台能力。
image.png1、最上层:核心模块
最上层是node api(标准库),提供http模块、流模块、文件模块等等,可以使用js直接调用。
2、中间层:C++ Bindings
在核心模块之下,底层模块为了更好的性能,采用 C/C++实现,而上层的 JavaScript 代码无法直接与 C/C++通信,因而需要一个桥梁(即 Binding)。
3、底层库:libuv
为 Node.js 量身打造,用 C 写的跨平台异步 I/O 库,提供了非阻塞的文件系统、DNS、网络、子进程、管道、信号、轮询和流式处理机制。
image.png
-
Libuv使用各平台提供的事件驱动模块实现异步(epoll, kqueue, IOCP, event ports)。他用来支持上层非文件io的模块。libuv把上层的事件和回调封装成io观察者(uv__io_t)放到底层的事件驱动模块。当事件触发的时候,libuv会执行io观察者中的回调。
-
Libuv实现一个线程池用来支持上层文件 I/O、DNS 查询以及用户层耗cpu的任务。
4、底层库:其它依赖库(C/C++库)
-
llhttp:用 TypeScript 和 C 写的轻量级 HTTP 解析库,比之前的http_parser快 1.5 倍,不含任何系统调用和内存分配(也不缓存数据),因此每个请求的内存占用极小
-
c-ares:一个 C 库,用来处理异步的 DNS 请求,对应 Node.js 中dns模块提供的resolve()系列方法
-
OpenSSL:一个通用的加密库,多用于网络传输中的 TLS 和 SSL 协议实现,对应 Node.js 中的tls、crypto模块
-
zlib:提供快速压缩和解压支持.
四、Node.js的工作流程
通过上面技术架构的了解,可以大概知道node的执行过程,把上面流程串一串形成如下工作流程。
image.png
1、工作流程的理解。
Application就是咱们写的代码,把它放在v8上面去运行。比如需要去读一个文件( I/O 操作),先回调函数将排到事件队列中,这时候libuv开一个线程去读文件。读完文件,操作系统会返回一个事件给event loop,event loop就把文件传回给v8,再给到代码。Node中的Event Loop(事件循环)是由底层的libuv库负责执。
执行过程中遇到 I/O 操作就交给 libuv 线程池中的某个 woker 来处理,结束之后 libuv 产生一个事件放入事件队列。事件循环处理到返回事件时,对应的回调函数才在主线程开始执行,主线程在此期间继续其它工作,而不阻塞等待。(咖啡店实例)
2、什么是Event Loop?
- 主线程执行栈全部任务执行完毕。
- 检查微任务队列,process.nextTick优先级最高,总是最先执行。
- 检查宏任务队列,提取一次任务推入执行栈,进行执行。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
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 (r != 0 && loop->stop_flag == 0) {
// 这里也会更新loop的time字段
uv__update_time(loop);
// 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
uv__run_timers(loop);
// 执行pending回调:执行延迟到下一个循环迭代的 I/O 回调。
ran_pending = uv__run_pending(loop);
// idle, prepare:仅系统内部使用。
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
// 执行模式是UV_RUN_ONCE时,如果没有pending节点,才会阻塞式poll io,默认模式也是
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(例如:文件可读了?读!http请求来了?几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
uv__io_poll(loop, timeout);
/* Run one final update on the provider_idle_time in case uv__io_poll
* returned because the timeout expired, but no events were received. This
* call will be ignored if the provider_entry_time was either never set (if
* the timeout == 0) or was already updated b/c an event was received.
*/
uv__metrics_update_idle_time(loop);
// 检测:setImmediate() 回调函数在这里执行。
uv__run_check(loop);
// 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。
uv__run_closing_handles(loop);
// 还有一次执行超时回调的机会,因为poll io阶段可能是因为定时器超时返回的。
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);
}
r = uv__loop_alive(loop);
// 只执行一次,退出循环,UV_RUN_NOWAIT表示在poll io阶段不会阻塞并且循环只执行一次
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
// 是因为调用了uv_stop退出的,重置flag
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
3、process.nextTick
为什么 process.nextTick的优先级这么高,node里面的Promise等微任务呢?
下面看看libuv定时器的调度回调函数:
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
heap_node = heap_min(timer_heap(loop));
if (heap_node == NULL)
break;
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
break;
uv_timer_stop(handle);
uv_timer_again(handle);
handle->timer_cb(handle); // 执行底层的回调函数
}
}
再看看V8里Js对nextTick回调的实现:
function _tickCallback() {
let tock;
do {
while (tock = nextTickQueue.shift()) {
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol]);
if (async_hook_fields[kDestroy] > 0)
emitDestroy(asyncId);
const callback = tock.callback;
if (tock.args === undefined)
callback();
else
Reflect.apply(callback, undefined, tock.args);
emitAfter(asyncId);
}
runMicrotasks();
} while (nextTickQueue.head !== null || emitPromiseRejectionWarnings());
tickInfo[kHasPromiseRejections] = 0;
}
其实,每次libuv执行一个阶段的事件后,都会执行上层的回调,都会执行next_tick注册的回调函数,执行完之后会执行runMicrotasks()函数。