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.png
1、最上层:核心模块

最上层是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?
  1. 主线程执行栈全部任务执行完毕。
  2. 检查微任务队列,process.nextTick优先级最高,总是最先执行。
  3. 检查宏任务队列,提取一次任务推入执行栈,进行执行。
   ┌───────────────────────────┐
┌─>│           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()函数。

你可能感兴趣的:(Node.js的原理分析)