美文网首页
Node.js的原理分析

Node.js的原理分析

作者: 喵一喵哈 | 来源:发表于2021-03-24 16:45 被阅读0次

    希望能对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的原理分析

          本文链接:https://www.haomeiwen.com/subject/niczcltx.html