Jacleklm's Blog

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

2020/07/15

感受:这本书更偏向于讲 NodeJS 的大纲和原理。所以本笔记只讲原理和case,对于API不熟的请翻文档,或看小卡的笔记, 或看poetries的笔记

第一章 Node 简介

Chrome 浏览器的组成和 Node 的组件组成

Node 的特点

  • 异步 I/O。eg. Ajax, fs.readFile 等,Node 中绝大数的操作都以异步的方式进行调用
  • 事件与回调函数。将前端中广泛成熟点时间机制引入后端,配合 IO 使用,eg. 监听 request 事件,然后执行回调函数
  • 单线程。保留了 JS 在浏览器中单线程的特点
    • 优点:不用在意状态的同步问题,没有死锁的存在,也没有上下文交互的性能开销
    • 弱点:
      • 无法利用多核 CPU
      • 错误会引起整个应用退出,代码健壮性值得考验
      • 大量计算占用 CPU 无法继续调用异步 I/O。eg. 长时间的 CPU 占用会导致后续的异步 IO 发不出调用。浏览器中类似的问题被 Web Worker 解决,Node 也采用类似的思路:子进程 child_process
    • 子进程的出现可以解决单线程在健壮性和无法利用多核 CPU 的问题。通过将计算分发到各个子进程,可以将大量斤算分解掉,再通过进程的事件消息来传递结果,可很好地保持应用模型的简单和依赖;通过 Master-Worker 的管理方式,也可以很好地管理各个工作的进程,以达到更高的健壮性
  • 跨平台。Node 基于 libuv 实现跨平台,可以在 Linux,Window 使用。它在操作系统和 Node 上层模块系统之间构建了一层平台层架构,即 libuv。libux 也是许多系统实现跨平台的基础组件

应用场景

  • I/O 密集
  • CPU 密集是否不合适?Node 没有提供多线程用于计算支持,但是仍有两个方式充分利用 CPU:
    • Node 可以通过编写 C++扩展的方式提高 CPU 利用,将一些 V8 不能做到性能极致的地方用 C++来实现
    • 用子进程的方式,将一部分进程用来计算,然后用进程通信传递结果,将计算和 I/O 分离

应用好处

用 Node 做 Web 开发,前端工程师在 HTTP 协议栈的两端能高效灵活开发,避免了 Java 的繁琐;另一方面,又利用 Java 作为后端接口和中间件,使其有良好的稳定性。两者取长补短

第二章 模块机制

CommonJS 规范

解决 JS 弱结构性的问题,愿景:希望 JS 能在任何地方运行
eg.

1
2
3
const math = require(‘math’)

exports.add = function() { xxx }

Node 模块的实现

引入模块的实现步骤:

  • 路径分析
  • 文件定位
  • 编译执行

Node 提供的模块称为核心模块;用户编写的模块称为文件模块。都会优先从缓存加载 Module._cache;缓存的是 Node 编译和执行后的对象

模块编译

核心模块部分在 Node 源代码的变异过程中,编译进了二进制执行文件;启动时就被直接加载进内存中;而文件模块是在运行时动态加载,需要完成走上述三个流程
定位到具体文件后,Node 会新建一个模块对象,然后根据路径载入并编译。对不同的文件扩展名,载入方式不同:

  • .js。通过 fs 模块读取文件后编译执行
  • .node。这是 C/C++编写的扩展文件,通过 process.dlopen()方法加载最后编译生成的文件(但其实 C/C++模块不用编译,它本来就是编译好的)。C/C++模块的优势是执行效率;但显然 JS 编写的模块开发速度更快
  • .json。fs 模块读取后,用 JSON.parse()解析返回结果
  • 其他。都被当成.js

核心模块 & 编译 & 编写核心模块

核心模块分为 C/C++编写的和 JS 编写的两部分。Node 的 buffer,crypto,evals,fs,os 等模块都是部分通过 C/C++编写的

C/C++扩展模块 & 加载 & 编写

CPU 性能更加

模块调用栈

包和 NPM

在模块之外,包和 NPM 是将模块联系起来的一种机制,是在模块的基础上进一步组织 JS 代码。包结构如下:

  • package.json
  • bin。存放可执行的二进制文件
  • lib。存放 JS 代码
  • doc
  • test。单元测试用例

已经很少用的 AMD 和 CMD 规范

第三章 异步 I/O

异步 I/O 实现现状

阻塞 IO 造成 CPU 等待浪费,非阻塞带来的麻烦事需要轮训去确认是否完成数据获取,它会让 CPU 处理状态判断,是对 CPU 资源的浪费
轮询的五种方案:read, select,poll, epoll(效率最高), kqueue。见 P53

Node 的异步 IO

事件循环、观察者、请求对象、I/O 线程池这四者共同构建成了 Node 异步 I/O 模型的基本要素
事件循环是异步实现的核心

  • Node 自身的执行模型是事件循环,正是它使得回调函数十分普遍。进程启动的时候,Node 会创建一个类似 whlie 的循环,每执行一次循环体的过程我们称为 Tick。如下图
  • (还有事件?) 那里即是观察者

请求对象 和 I/O 线程池

没太看懂。反正整个异步 IO 流程如下:

输入输出完成端口(Input/Output Completion Port,IOCP)

  • 这里的单线程与 IO 线程池看起来有些悖论的杨祖。由于我们知道 JS 是单线程的,所以按常识容易理解为它不能充分利用多核 CPU。事实上,在 Node 中,除了 JS 是单线程外,Node 自身其实是多线程的,只是 IO 线程使用的 CPU 较少
  • 另一点需要重视的观点则是,除了用户代码无法并行执行外,所有的 IO(磁盘 IO 和网络 IO 等)则是可以并行起来的

非 IO 的异步 API

Node 中与 IO 无关的异步 API:setTimeout(), setInterval(), setImmediate(), process.nextTick()

定时器

setTimeout()和 setInterval()和浏览器中是一致的,分别用于单次和多次定时执行任务。

  • 原理。它们的原理与异步 IO 比较类似,只是不需要 IO 线程池的参与。调用 setTimeout()或 setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次 Tick 执行时,会从该红黑树中迭代去除定时器对象,检查是否超过时间,如果超过,就形成一个事件,它的回调函数将立即执行。这个过程包含动用红黑树,创建定时器对象和迭代等操作,较为浪费性能
  • 问题。它并非精准的。尽管时间循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,它也许已经超时很久了。eg. 通过 setTimeout 设定一个任务在 10ms 后执行,但是在 9ms 后,有一个任务占用了 5ms 的 CPU 时间片,再次轮到定时器执行时,时间就已经过期 4ms 了

process.nextTick() 和 setImmediate()

  • 每次调用 process.nextTick 函数,只会将回调函数放入队列中,在下一轮 Tick 的时候取出运行。定时器中采用红黑树操作的时间复杂度为 O(lg(n)), 而 nextTick 为 O(1),更为高效
  • setImmediate()和 process.nextTick 十分类似,都是将回调函数延迟执行。但是执行优先级没 process.nextTick 高。似乎都属于微任务?

事件驱动与高性能服务器


图3-15 利用Node构建Wen服务器的流程图

第四章 异步编程

函数式编程

  • 高阶函数:可以把函数作为参数,或是将函数作为返回值的函数。eg. JS中的forEach, some, reduce等
  • 偏函数:指创建一个调用另外一个部分——参数或变量已经预置的函数——的函数的用法。个人理解:偏函数会返返回一个函数,返回的函数的一部分是由偏函数的参数生成的。eg. 封装一个判断类型的函数
    1
    2
    3
    4
    5
    6
    7
    let isType = function(type) {
    return function(obj) {
    return Object.toString.call(obj) === '[object ' + type + ']'
    }
    }
    Let isString = isType('String')
    isString('test') // true

异步编程的优势和难点

  • 优势:基于时间驱动的非阻塞IO模型
  • 难点
    • 异常处理
    • 函数嵌套太深
    • 阻塞代码
    • 多线程的编程。有了child_process
    • 异步转同步

异步并发控制

第五章 【重点】内存控制

【重点】V8的垃圾回收机制与内存限制

第六章 理解Buffer

CATALOG
  1. 1. 第一章 Node 简介
    1. 1.1. Chrome 浏览器的组成和 Node 的组件组成
    2. 1.2. Node 的特点
    3. 1.3. 应用场景
    4. 1.4. 应用好处
  2. 2. 第二章 模块机制
    1. 2.1. CommonJS 规范
    2. 2.2. Node 模块的实现
      1. 2.2.1. 引入模块的实现步骤:
      2. 2.2.2. 模块编译
    3. 2.3. 核心模块 & 编译 & 编写核心模块
    4. 2.4. C/C++扩展模块 & 加载 & 编写
    5. 2.5. 模块调用栈
    6. 2.6. 包和 NPM
    7. 2.7. 已经很少用的 AMD 和 CMD 规范
  3. 3. 第三章 异步 I/O
    1. 3.1. 异步 I/O 实现现状
    2. 3.2. Node 的异步 IO
      1. 3.2.1. 请求对象 和 I/O 线程池
    3. 3.3. 非 IO 的异步 API
      1. 3.3.1. 定时器
      2. 3.3.2. process.nextTick() 和 setImmediate()
    4. 3.4. 事件驱动与高性能服务器
  4. 4. 第四章 异步编程
    1. 4.1. 函数式编程
    2. 4.2. 异步编程的优势和难点
    3. 4.3. 异步并发控制
  5. 5. 第五章 【重点】内存控制
    1. 5.1. 【重点】V8的垃圾回收机制与内存限制
  6. 6. 第六章 理解Buffer