《深入浅出Node.js》读书笔记

135 阅读6分钟

Node简介

mindmap
      Node的特点
            异步I/O
            事件与回调函数
            单线程
            跨平台
mindmap
      Node的应用场景
          I/O密集型
          通过合理调度,适用CPU密集型
          与遗留系统和平共处
          分布式应用
mindmap
      Node受欢迎的原因
          前后端编程语言环境统一
          Node带来的高性能I/O用于实时应用
          并行I/O使得使用者可以更高效地利用分布式环境
          并行I/O,有效利用稳定接口提升web渲染能力
          云计算平台提供Node支持
          游戏开发领域
          工具类应用

模块机制

CommonJS规范

  • 模块引用
  • 模块定义
  • 模块标识

Node的模块实现

引入模块流程:

graph TD
路径分析 --> 文件定位 --> 编译执行

模块分两类:

  • 核心模块

在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

  • 文件模块

在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

Node对引入过的模块都会进行缓存,以减少二次引入时的开销。缓存的是编译和执行之后的对象。

模块调用栈

C/C++内建模块属于最底层的模块,它属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。如果你不是非常了解要调用的C/C++内建模块,请尽量避免通过Process.binding()方法直接调用,这是不推荐的。

JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,它不需要跟底层打交道,但是又十分重要。

image.png

异步I/O

为什么用异步I/O

  • 用户体验
  • 资源分配

事件循环

Node自身的执行模型

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出流程。

image.png

image.png

以下资料参考:

juejin.cn/post/701030…

image.png

image.png

image.png

非I/O的异步API

  • 定时器
    • setTimeout
    • setInterval

image.png

  • process.nextTick()

每次调用process.nextTick方法, 只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为0(lg(n)),nextTick()的时间复杂度为0(1),相较之下,process.nextTick()更高效。

  • setImmediate
    和process.nextTick()类似,都是将回调函数延迟执行。
    但process.nextTick()中的回调函数执行的优先级要高于setImmediate()

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完毕,而setImmediate()在每轮循环中执行链表中的一个回调函数。

image.png

异步编程

mindmap
     异步编程的难点
          异常处理
          函数嵌套过深
          阻塞代码
          多线程编程
          异步转同步
mindmap
      异步编程解决方案
          事件发布/订阅模式
          Promise/Deferred模式
          流程控制库

内存控制

V8的垃圾回收机制

V8的垃圾回收策略主要基于分代式垃圾回收机制。

在V8中,主要将内存分为新生代和老生代。新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象

image.png

新生代的对象主要通过Scavenge算法进行垃圾回收(空间换时间)

image.png

对象从新生代移动到老生代中的过程称为晋升。 对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。

image.png

image.png

老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。
Mark-Sweep(标记清除): 在标记阶段遍历堆中所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。

image.png

Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。

为解决Mark-Sweep的内存碎片问题,Mark-Compact被提出来。Mark-Compact是标记整理的意思。差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后。直接清理掉边界外的内存。

image.png

image.png

在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代晋升过来的对象进行分配时才使用Mark-Compact。

为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为 “全停顿”

为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,也就是拆分为许多小“步进”,没做完一“步进”,就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直接标记阶段完成。

image.png

V8后续还引入了延迟清理增量式整理,让清理与整理动作也变成增量式的。同时还计划引入并行标记并行清理,进一步利用多核性能降低每次停顿的时间。

触发垃圾回收的有:

  • 作用域:函数作用域,with、全局作用域
  • 闭包

Node中的内存并非都是通过V8进行分配,我们将哪些不是通过V8分配的内存称为堆外内存

Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。

内存泄漏

造成内存泄漏的原因有如下几个:

  • 缓存
  • 队列消费不及时
  • 作用域未释放

注意:

  • 慎将内存当做缓存
  • 关注队列状态

理解Buffer

image.png

为了高效的使用申请来的内存,Node采用了slab分配机制(动态内存管理机制)。 slab就是一块申请好的固定大小的内存区域。有如下3种状态:

  • full: 完全分配状态
  • partial: 部分分配状态
  • empty: 没有被分配状态

Node以8KB为界限来区分Buffer是大对象还是小对象。这个8KB的值也就是每个slab的大小值。

网络编程

构建网络应用

玩转进程

服务模型的变迁

  1. 同步
  2. 复制进程:每个连接都需要一个进程来服务
  3. 多线程: 一个线程服务一个请求
  4. 事件驱动

测试

产品化