前端应该了解的 Node.Js 知识及原理浅析

2022-08-0420:34:35WEB前端开发Comments1,217 views字数 23064阅读模式

前端应该了解的 Node.Js 知识及原理浅析文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

我正在参与掘金创作者训练营第5期,点击了解活动详情文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

node.js 初探

Node.js 是一个 JS 的服务端运行环境,简单的来说,它是在 JS 语言规范的基础上,封装了一些服务端的运行时对象,让我们能够简单实现非常多的业务功能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

如果我们只使用 JS 的话,实际上只是能进行一些简单的逻辑运算。node.js 就是基于 JS 语法增加与操作系统之间的交互。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

node.js 的安装

我们可以使用多种方式来安装 node.js,node.js 本质上也是一种软件,我们可以使用直接下载二进制安装文件安装,通过系统包管理进行安装或者通过源码自行编译均可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

一般来讲,对于个人开发的电脑,我们推荐直接通过 node.js 官网的二进制安装文件来安装。对于打包上线的一些 node.js 环境,也可以通过二进制编译的形式来安装。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

安装成功之后,我们的 node 命令就会自动加入我们的系统环境变量 path 中,我们就能直接在全局使用 node 命令访问到我们刚才安装的 node 可执行命令行工具。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

node.js 版本切换在个人电脑上,我们可以安装一些工具,对 node.js 版本进行切换,例如 nvm 和 n文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

nvm 的全称就是 node version manager,意思就是能够管理 node 版本的一个工具,它提供了一种直接通过 shell 执行的方式来进行安装。简单来说,就是通过将多个 node 版本安装在指定路径,然后通过 nvm 命令切换时,就会切换我们环境变量中 node 命令指定的实际执行的软件路径。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

安装成功之后,我们就能在当前的操作系统中使用多个 node.js 版本。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

包管理工具 npm

curl -o- https://raw.githubusercontent.com/nvm- sh/nvm/v0.35.3/install.sh | bash文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

我们对 npm 应该都比较熟悉了,它是 node.js 内置的一款工具,目的在于安装和发布符合 node.js 标准的模块,从而实现社区共建的目的繁荣整个社区。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

npx 是 npm@5 之后新增的一个命令,它使得我们可以在不安装模块到当前环境的前提下,使用一些 cli 功能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

例如 npx create-react-app some-repo文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

node.js 的底层依赖

node.js 的主要依赖子模块有以下内容:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

V8 引擎

主要是 JS 语法的解析,有了它才能识别 JS 语法文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

libuv

c 语言实现的一个高性能异步非阻塞 IO 库,用来实现 node.js 的事件循环文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

http-parser/llhttp

底层处理 http 请求,处理报文, 解析请求包等内容文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

openssl

处理加密算法,各种框架运用广泛文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

zlib

处理压缩等内容 node.js 常⻅内置模块文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

主要模块

node.js 中最主要的内容,就是实现了一套 CommonJS 的模块化规范,以及内置了一些常⻅的模块。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

fs:

文件系统,能够读取写入当前安装系统环境中硬 盘的数据\文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

path:

路径系统,能够处理路径之间的问题文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

crypto:

加密相关模块,能够以标准的加密方式对我 们的内容进行加解密文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

dns:

处理 dns 相关内容,例如我们可以设置 dns 服 务器等等\文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

http:

设置一个 http 服务器,发送 http 请求,监听 响应等等文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

readline:

读取 stdin 的一行内容,可以读取、增加、 删除我们命令行中的内容\文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

os:

操作系统层面的一些 api,例如告诉你当前系统类 型及一些参数文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

vm:

一个专⻔处理沙箱的虚拟机模块,底层主要来调 用 v8 相关 api 进行代码解析。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

V8 引擎:

github.com/v8/v8 chromium.googlesource.com/v8/v8.git文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

引擎只是解析层面,具体的上层还有许多具体环境的封装。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Debug & 内存泄漏

对于浏览器的 JS 代码来说,我们可以通过断点进行分步调试,每一步打印当前上下文中的变量结果,来定位具体问题出现在哪一步。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

我们可以借助 VSCode 或者自行打断点的形式,来进行分步 node.js 调试。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

对于 JS 内存泄漏,我们也可以使用同样的道理,借助工具,打印每次的内存快照,对比得出代码中的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

另一种 JS 解析引擎 quickjs

quickjs 是一个 JS 的解析引擎,轻量代码量也不大,与之功能类似的就是 V8 引擎。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

他最大的特点就是,非常非常轻量,这点从源码中也能体现,事实上并没有太多的代码,它的主要特点和优势:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 轻量而且易于嵌入: 只需几个C文件,没有外部依赖,一个x86下的简单的“hello world”程序只要180 KiB
  2. 具有极低启动时间的快速解释器: 在一台单核的台式PC上,大约在100秒内运行ECMAScript 测试套件156000次的运行时实例完整生命周期在不到300微秒的时间内完成。
  3. 几乎完整实现ES2019支持,包括: 模块,异步生成器和和完整Annex B(MPEG-2 transport stream format格式)支持 (传统的Web兼容性)。许多ES2020中带来的特性也依然会被支持。 通过100%的ECMAScript Test Suite测试。 可以将Javascript源编译为没有外部依赖的可执行文件。

另一类 JS 运行时服务端环境 deno

deno 是一类类似于 node.js 的 JS 运行时环境,同时它也是由 node.js 之父一手打造出来的,他和 node.js 比有什么区别呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

相同点:

  • deno 也是基于 V8 ,上层封装一些系统级别的调用我们的 deno 应用也可以使用 JS 开发

不同点:

  • deno 基于 rust 和 typescript 开发一些上层模块,所以我们可以直接在 deno 应用中书写 ts
  • deno 支持从 url 加载模块,同时支持 top level await 等特性

全局对象解析

JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

在 Node.js 我们可以直接访问到 global 的属性,而不需要在应用中包含它。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

全局对象和全局变量

global 最根本的作用是作为全局变量的宿主。按照 ECMAScript 的定义,满足以下条 件的变量是全局变量:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

在最外层定义的变量; 全局对象的属性; 隐式定义的变量(未定义直接赋值的变量)。 当你定义一个全局变量时,这个变量同时也会成为全局对象的属性,反之亦然。需要注 意的是,在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的, 而模块本身不是最外层上下文。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

注意: 永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染 命名空间,提高代码的耦合风险。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

__filename

__filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。 如果在模块中,返回的值是模块文件的路径。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

console.log( __filename );
复制代码

__dirname

__dirname 表示当前执行脚本所在的目录。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

console.log( __dirname );
复制代码

setTimeout(cb, ms)

setTimeout(cb, ms) 全局函数在指定的毫秒(ms)数后执行指定函数(cb)。:setTimeout() 只执行一次指定函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

返回一个代表定时器的句柄值。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

function printHello(){
   console.log( "Hello, World!");
}
// 两秒后执行以上函数
setTimeout(printHello, 2000);

复制代码

clearTimeout、setInterval、clearInterval、console 在js中比较常见,故不做展开。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

process

process 是一个全局变量,即 global 对象的属性。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

它用于描述当前Node.js 进程状态的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候,少不了要 和它打交道。下面将会介绍 process 对象的一些最常用的成员方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. exit 当进程准备退出时触发。
  2. beforeExit 当 node 清空事件循环,并且没有其他安排时触发这个事件。通常来说,当没有进程安排时 node 退出,但是 ‘beforeExit’ 的监听器可以异步调用,这样 node 就会继续执行。
  3. uncaughtException 当一个异常冒泡回到事件循环,触发这个事件。如果给异常添加了监视器,默认的操作(打印堆栈跟踪信息并退出)就不会发生。
  4. Signal 事件 当进程接收到信号时就触发。信号列表详见标准的 POSIX 信号名,如 SIGINT、SIGUSR1 等。
process.on('exit', function(code) {
  // 以下代码永远不会执行
  setTimeout(function() {
    console.log("该代码不会执行");
  }, 0);
   
  console.log('退出码为:', code);
});
console.log("程序执行结束");

复制代码

退出的状态码

  1. Uncaught Fatal Exception 有未捕获异常,并且没有被域或 uncaughtException 处理函数处理。
  2. Internal JavaScript Parse Error JavaScript的源码启动 Node 进程时引起解析错误。非常罕见,仅会在开发 Node 时才会有。
  3. Internal JavaScript Evaluation Failure JavaScript 的源码启动 Node 进程,评估时返回函数失败。非常罕见,仅会在开发 Node 时才会有。
  4. Fatal Error V8 里致命的不可恢复的错误。通常会打印到 stderr ,内容为: FATAL ERROR
  5. Non-function Internal Exception Handler 未捕获异常,内部异常处理函数不知为何设置为on-function,并且不能被调用。
  6. Internal Exception Handler Run-Time Failure 未捕获的异常, 并且异常处理函数处理时自己抛出了异常。例如,如果 process.on(‘uncaughtException’) 或 domain.on(‘error’) 抛出了异常。
  7. Invalid Argument 可能是给了未知的参数,或者给的参数没有值。
  8. Internal JavaScript Run-Time Failure JavaScript的源码启动 Node 进程时抛出错误,非常罕见,仅会在开发 Node 时才会有。
  9. Invalid Debug Argument 设置了参数–debug 和/或 –debug-brk,但是选择了错误端口。
  10. Signal Exits 如果 Node 接收到致命信号,比如SIGKILL 或 SIGHUP,那么退出代码就是128 加信号代码。这是标准的 Unix 做法,退出信号代码放在高位。
// 输出到终端
process.stdout.write("Hello World!" + "\n");
 
// 通过参数读取
process.argv.forEach(function(val, index, array) {
   console.log(index + ': ' + val);
});
 
// 获取执行路局
console.log(process.execPath);
 
// 平台信息
console.log(process.platform);

复制代码

试试看这段代码输出什么

// this in NodeJS global scope is the current module.exports object, not the global object.

console.log(this);    // {}

module.exports.foo = 5;

console.log(this);   // { foo:5 }
复制代码

Buffer

在了解Nodejs的Buffer之前, 先看几个基本概念。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

背景知识

1. ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

可以把它理解为一块内存, 具体存什么需要其他的声明。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

new ArrayBuffer(length)

// 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节。
// 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。
// 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。
复制代码

ex. 比如这段代码, 可以执行一下看看输出什么文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var buffer = new ArrayBuffer(8);
var view = new Int16Array(buffer);

console.log(buffer);
console.log(view);
复制代码

2. Unit8Array

Uint8Array 数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。 创建完后,可以对象的方式或使用数组下标索引的方式引用数组中的元素。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

// 来自长度
var uint8 = new Uint8Array(2);
uint8[0] = 42;
console.log(uint8[0]); // 42
console.log(uint8.length); // 2
console.log(uint8.BYTES_PER_ELEMENT); // 1

// 来自数组
var arr = new Uint8Array([21,31]);
console.log(arr[1]); // 31

// 来自另一个 TypedArray
var x = new Uint8Array([21, 31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21

复制代码

3. ArrayBuffer 和 TypedArray

TypedArray: Unit8Array, Int32Array这些都是TypedArray, 那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

ArrayBuffer 本身只是一个 0 和 1 存放在一行里面的一个集合,ArrayBuffer 不知道第一个和第二个元素在数组中该如何分配。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

为了能提供上下文,我们需要将其封装在一个叫做 View 的东西里面。这些在数据上的 View 可以被添加进确定类型的数组,而且我们有很多种确定类型的数据可以使用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 总结

总之, ArrayBuffer 基本上扮演了一个原生内存的角色.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

NodeJs Buffer

Buffer 类以一种更优化、更适合 Node.js 用例的方式实现了 Uint8Array API.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Buffer 的大小在被创建时确定,且无法调整。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

基本使用

// 创建一个长度为 10、且用 0 填充的 Buffer。
const buf1 = Buffer.alloc(10);

// 创建一个长度为 10、且用 0x1 填充的 Buffer。 
const buf2 = Buffer.alloc(10, 1);

// 创建一个长度为 10、且未初始化的 Buffer。
// 这个方法比调用 Buffer.alloc() 更快,
// 但返回的 Buffer 实例可能包含旧数据,
// 因此需要使用 fill() 或 write() 重写。
const buf3 = Buffer.allocUnsafe(10);

// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。
const buf4 = Buffer.from([1, 2, 3]);

// 创建一个包含 UTF-8 字节  的 Buffer。
const buf5 = Buffer.from('tést');

复制代码

tips

当调用 Buffer.allocUnsafe() 时,被分配的内存段是未初始化的(没有用 0 填充)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

虽然这样的设计使得内存的分配非常快,但已分配的内存段可能包含潜在的敏感旧数据。 使用通过 Buffer.allocUnsafe() 创建的没有被完全重写内存的 Buffer ,在 Buffer内存可读的情况下,可能泄露它的旧数据。 虽然使用 Buffer.allocUnsafe() 有明显的性能优势,但必须额外小心,以避免给应用程序引入安全漏洞。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Buffer 与字符编码

Buffer 实例一般用于表示编码字符的序列,比如 UTF-8 、 UCS2 、 Base64 、或十六进制编码的数据。 通过使用显式的字符编码,就可以在 Buffer 实例与普通的 JavaScript 字符串之间进行相互转换。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

const buf = Buffer.from('hello world', 'ascii');

console.log(buf)

// 输出 68656c6c6f20776f726c64
console.log(buf.toString('hex'));

// 输出 aGVsbG8gd29ybGQ=
console.log(buf.toString('base64'));
复制代码

Buffer 与字符编码

Buffer 实例一般用于表示编码字符的序列,比如 UTF-8 、 UCS2 、 Base64 、或十六进制编码的数据。 通过使用显式的字符编码,就可以在 Buffer 实例与普通的 JavaScript 字符串之间进行相互转换。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

const buf = Buffer.from('hello world', 'ascii');

console.log(buf)

// 输出 68656c6c6f20776f726c64
console.log(buf.toString('hex'));

// 输出 aGVsbG8gd29ybGQ=
console.log(buf.toString('base64'));
复制代码

Node.js 目前支持的字符编码包括:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 'ascii' - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
  2. 'utf8' - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
  3. 'utf16le' - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  4. 'ucs2' - 'utf16le' 的别名。
  5. 'base64' - Base64 编码。当从字符串创建 Buffer 时,按照 RFC4648 第 5 章的规定,这种编码也将正确地接受 “URL 与文件名安全字母表”。
  6. 'latin1' - 一种把 Buffer 编码成一字节编码的字符串的方式(由 IANA 定义在 RFC1345 第 63 页,用作 Latin-1 补充块与 C0/C1 控制码)。
  7. 'binary' - 'latin1' 的别名。
  8. 'hex' - 将每个字节编码为两个十六进制字符。

Buffer 内存管理

在介绍 Buffer 内存管理之前,我们要先来介绍一下 Buffer 内部的 8K 内存池。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

8K 内存池

  1. 在 Node.js 应用程序启动时,为了方便地、高效地使用 Buffer,会创建一个大小为 8K 的内存池。
Buffer.poolSize = 8 * 1024; // 8K
var poolSize, poolOffset, allocPool;

// 创建内存池
function createPool() {
  poolSize = Buffer.poolSize;
  allocPool = createUnsafeArrayBuffer(poolSize);
  poolOffset = 0;
}

createPool();
复制代码
  1. 在 createPool() 函数中,通过调用 createUnsafeArrayBuffer() 函数来创建 poolSize(即8K)的 ArrayBuffer 对象。createUnsafeArrayBuffer() 函数的实现如下:
function createUnsafeArrayBuffer(size) {
  zeroFill[0] = 0;
  try {
    return new ArrayBuffer(size); // 创建指定size大小的ArrayBuffer对象,其内容被初始化为0。
  } finally {
    zeroFill[0] = 1;
  }
}
复制代码

这里你只需知道 Node.js 应用程序启动时,内部有个 8K 的内存池即可。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 前面简单介绍了 ArrayBuffer 和 Unit8Array 相关的基础知识,而 ArrayBuffer 的应用在 8K 的内存池部分的已经介绍过了。那接下来当然要轮到 Unit8Array 了,我们再来回顾一下它的语法:
Uint8Array(length);
Uint8Array(typedArray);
Uint8Array(object);
Uint8Array(buffer [, byteOffset [, length]]);
复制代码

其实除了 Buffer 类外,还有一个 FastBuffer 类,该类的声明如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

class FastBuffer extends Uint8Array {
  constructor(arg1, arg2, arg3) {
    super(arg1, arg2, arg3);
  }
}
复制代码

是不是知道 Uint8Array 用在哪里了,在 FastBuffer 类的构造函数中,通过调用 Uint8Array(buffer [, byteOffset [, length]]) 来创建 Uint8Array 对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 那么现在问题来了,FastBuffer 有什么用?它和 Buffer 类有什么关系?带着这两个问题,我们先来一起分析下面的简单示例:
const buf = Buffer.from('semlinker');
console.log(buf); // <Buffer 73 65 6d 6c 69 6e 6b 65 72>

复制代码

为什么输出了一串数字, 我们创建的字符串呢? 来看一下源码文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

/**
 * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError
 * if value is a number.
 * Buffer.from(str[, encoding])
 * Buffer.from(array)
 * Buffer.from(buffer)
 * Buffer.from(arrayBuffer[, byteOffset[, length]])
 **/
Buffer.from = function from(value, encodingOrOffset, length) {
  if (typeof value === "string") return fromString(value, encodingOrOffset);
  // 处理其它数据类型,省略异常处理等其它代码
  if (isAnyArrayBuffer(value))
    return fromArrayBuffer(value, encodingOrOffset, length);
  var b = fromObject(value);
};
复制代码

可以看出 Buffer.from() 工厂函数,支持基于多种数据类型(string、array、buffer 等)创建 Buffer 对象。对于字符串类型的数据,内部调用 fromString(value, encodingOrOffset) 方法来创建 Buffer 对象。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

是时候来会一会 fromString() 方法了,它内部实现如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

function fromString(string, encoding) {
  var length;
  if (typeof encoding !== "string" || encoding.length === 0) {
    if (string.length === 0) return new FastBuffer();
    // 若未设置编码,则默认使用utf8编码。
    encoding = "utf8"; 
    // 使用 buffer binding 提供的方法计算string的长度
    length = byteLengthUtf8(string);
  } else {
	// 基于指定的 encoding 计算string的长度
    length = byteLength(string, encoding, true);
    if (length === -1)
      throw new errors.TypeError("ERR_UNKNOWN_ENCODING", encoding);
    if (string.length === 0) return new FastBuffer();
  }

  // 当字符串所需字节数大于4KB,则直接进行内存分配
  if (length >= Buffer.poolSize >>> 1)
    // 使用 buffer binding 提供的方法,创建buffer对象
    return createFromString(string, encoding);

  // 当剩余的空间小于所需的字节长度,则先重新申请8K内存
  if (length > poolSize - poolOffset)
    // allocPool = createUnsafeArrayBuffer(8K); poolOffset = 0;
    createPool(); 
  // 创建 FastBuffer 对象,并写入数据。
  var b = new FastBuffer(allocPool, poolOffset, length);
  const actual = b.write(string, encoding);
  if (actual !== length) {
    // byteLength() may overestimate. That's a rare case, though.
    b = new FastBuffer(allocPool, poolOffset, actual);
  }
  // 更新pool的偏移
  poolOffset += actual;
  alignPool();
  return b;
复制代码

所以我们得到这样的结论文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 当未设置编码的时候,默认使用 utf8 编码;
  2. 当字符串所需字节数大于4KB,则直接进行内存分配;
  3. 当字符串所需字节数小于4KB,但超过预分配的 8K 内存池的剩余空间,则重新申请 8K 的内存池;
  4. 调用 new FastBuffer(allocPool, poolOffset, length) 创建 FastBuffer 对象,进行数据存储,数据成功保存后,会进行长度校验、更新 poolOffset 偏移量和字节对齐等操作。

事件循环模型

什么是事件循环

事件循环使 Node.js 可以通过将操作转移到系统内核中来执行非阻塞 I/O 操作(尽管 JavaScript 是单线程的)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

由于大多数现代内核都是多线程的,因此它们可以处理在后台执行的多个操作。 当这些操作之一完成时,内核会告诉 Node.js,以便可以将适当的回调添加到轮询队列中以最终执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Node.js 启动时,它将初始化事件循环,处理提供的输入脚本,这些脚本可能会进行异步 API 调用,调度计时器或调用 process.nextTick, 然后开始处理事件循环。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
复制代码

每个阶段都有一个要执行的回调 FIFO 队列。 尽管每个阶段都有其自己的特殊方式,但是通常,当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或执行回调的最大数量为止。 当队列已为空或达到回调限制时,事件循环将移至下一个阶段。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
  2. pending callbacks:执行推迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare, :仅在内部使用。
  4. poll:取出新完成的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调,计时器调度的回调和 setImmediate 之外,几乎所有这些回调) 适当时,node 将在此处阻塞。
  5. check:在这里调用 setImmediate 回调。
  6. close callbacks:一些关闭回调,例如 socket.on('close', ...)

在每次事件循环运行之间,Node.js 会检查它是否正在等待任何异步 I/O 或 timers,如果没有,则将其干净地关闭。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

各阶段详细解析

timers 计时器阶段

计时器可以在回调后面指定时间阈值,但这不是我们希望其执行的确切时间。 计时器回调将在经过指定的时间后尽早运行。 但是,操作系统调度或其他回调的运行可能会延迟它们,即执行的实际时间不确定。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});
复制代码

当事件循环进入 poll 阶段时,它有一个空队列(fs.readFile 尚未完成),因此它将等待直到达到最快的计时器 timer 阈值为止。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

等待 95 ms 过去时,fs.readFile 完成读取文件,并将需要 10ms 完成的其回调添加到轮询 (poll) 队列并执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

回调完成后,队列中不再有回调,此时事件循环已达到最早计时器 (timer) 的阈值 (100ms),然后返回到计时器 (timer) 阶段以执行计时器的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

pending callbacks 阶段

此阶段执行某些系统操作的回调,例如 TCP 错误,平时无需关注。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

轮询 poll 阶段

轮询阶段具有两个主要功能:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 计算应该阻塞并 I/O 轮询的时间
  2. 处理轮询队列 (poll queue) 中的事件

当事件循环进入轮询 (poll) 阶段并且没有任何计时器调度 (timers scheduled) 时,将发生以下两种情况之一:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 如果轮询队列 (poll queue) 不为空,则事件循环将遍历其回调队列,使其同步执行,直到队列用尽或达到与系统相关的硬性限制为止。
  2. 如果轮询队列为空,则会发生以下两种情况之一: 2.1 如果已通过 setImmediate 调度了脚本,则事件循环将结束轮询 poll 阶段,并继续执行 check 阶段以执行那些调度的脚本。 2.2 如果脚本并没有 setImmediate 设置回调,则事件循环将等待 poll 队列中的回调,然后立即执行它们。

一旦轮询队列 (poll queue) 为空,事件循环将检查哪些计时器 timer 已经到时间。 如果一个或多个计时器 timer 准备就绪,则事件循环将返回到计时器阶段,以执行这些计时器的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

检查阶段 check

此阶段允许在轮询 poll 阶段完成后立即执行回调。 如果轮询 poll 阶段处于空闲,并且脚本已使用 setImmediate 进入 check 队列,则事件循环可能会进入 check 阶段,而不是在 poll 阶段等待。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

setImmediate 实际上是一个特殊的计时器,它在事件循环的单独阶段运行。 它使用 libuv API,该 API 计划在轮询阶段完成后执行回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

通常,在执行代码时,事件循环最终将到达轮询 poll 阶段,在该阶段它将等待传入的连接,请求等。但是,如果已使用 setImmediate 设置回调并且轮询阶段变为空闲,则它将将结束并进入 check 阶段,而不是等待轮询事件。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

注意:setImmediate为实验性方法,可能不会被批准成为标准,目前只有最新版本的 Internet Explorer 和 Node.js 0.10+ 实现了该方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

close callbacks 阶段

如果套接字或句柄突然关闭(例如 socket.destroy),则在此阶段将发出 'close' 事件。 否则它将通过 process.nextTick 发出。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

setImmediate 和 setTimeout 的区别

setImmediate 和 setTimeout 相似,但是根据调用时间的不同,它们的行为也不同。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  • setImmediate 设计为在当前轮询 poll 阶段完成后执行脚本。
  • setTimeout 计划在以毫秒为单位的最小阈值过去之后运行脚本。

Tips: 计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是主模块中调用的,则时序将受到进程性能的限制.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

来看两个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. 在主模块中执行

    两者的执行顺序是不固定的, 可能timeout在前, 也可能immediate在前文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

    setTimeout(() => {
     console.log('timeout');
     }, 0);
    
     setImmediate(() => {
     console.log('immediate');
     });
    复制代码
  2. 在同一个I/O回调里执行

    setImmediate总是先执行文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

    const fs = require('fs');
    
    fs.readFile(__filename, () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0);
        setImmediate(() => {
            console.log('immediate');
        });
    });
    复制代码

问题:那为什么在外部 (比如主代码部分 mainline) 这两者的执行顺序不确定呢?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

解答:在 主代码 部分执行 setTimeout 设置定时器 (此时还没有写入队列),与 setImmediate 写入 check 队列。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

mainline 执行完开始事件循环,第一阶段是 timers,这时候 timers 队列可能为空,也可能有回调; 如果没有那么执行 check 队列的回调,下一轮循环在检查并执行 timers 队列的回调; 如果有就先执行 timers 的回调,再执行 check 阶段的回调。因此这是 timers 的不确定性导致的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

process.nextTick

process.nextTick 从技术上讲不是事件循环的一部分。 相反,无论事件循环的当前阶段如何,都将在当前操作完成之后处理 nextTickQueue文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

process.nextTick 和 setImmediate 的区别

  • process.nextTick 在同一阶段立即触发
  • setImmediate fires on the following iteration or 'tick' of the event loop (在事件循环接下来的阶段迭代中执行 - check 阶段)。

nextTick在事件循环中的位置

           ┌───────────────────────────┐
        ┌─>│           timers          │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        │  │     pending callbacks     │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        |  |     idle, prepare|  └─────────────┬─────────────┘
  nextTickQueue     nextTickQueue
        |  ┌─────────────┴─────────────┐
        |  │           poll            │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        │  │           check           │
        │  └─────────────┬─────────────┘
        │           nextTickQueue
        │  ┌─────────────┴─────────────┐
        └──┤       close callbacks     │
           └───────────────────────────┘
复制代码

Microtasks 微任务

在 Node 领域,微任务是来自以下对象的回调:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. process.nextTick()
  2. then()

在主线结束后以及事件循环的每个阶段之后,立即运行微任务回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

resolved 的 promise.then 回调像微处理一样执行,就像 process.nextTick 一样。 虽然,如果两者都在同一个微任务队列中,则将首先执行 process.nextTick 的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

优先级 process.nextTick > promise.then文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

执行代码看看输出顺序

async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout0')
    setTimeout(function () {
        console.log('setTimeout1');
    }, 0);
    setImmediate(() => console.log('setImmediate'));
}, 0)

process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function (resolve) {
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function () {
    console.log('promise3')
})
console.log('script end')
复制代码

Events

events模块是node的核心模块之一,几乎所有常用的node模块都继承了events模块,比如http、fs等。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

模块本身非常简单,API虽然也不少,但常用的就那么几个,这里举几个简单例子。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

例子1:单个事件监听器

var EventEmitter = require('events');

class Man extends EventEmitter {}

var man = new Man();

man.on('wakeup', function(){
    console.log('man has woken up');
});

man.emit('wakeup');
// 输出如下:
// man has woken up
复制代码

例子2:同个事件,多个事件监听器

可以看到,事件触发时,事件监听器按照注册的顺序执行。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var EventEmitter = require('events');

class Man extends EventEmitter {}

var man = new Man();

man.on('wakeup', function(){
    console.log('man has woken up');
});

man.on('wakeup', function(){
    console.log('man has woken up again');
});

man.emit('wakeup');

// 输出如下:
// man has woken up
// man has woken up again
复制代码

例子3:只运行一次的事件监听器

var EventEmitter = require('events');

class Man extends EventEmitter {}

var man = new Man();

man.on('wakeup', function(){
    console.log('man has woken up');
});

man.once('wakeup', function(){
    console.log('man has woken up again');
});

man.emit('wakeup');
man.emit('wakeup');

// 输出如下:
// man has woken up
// man has woken up again
// man has woken up
复制代码

例子4:注册事件监听器前,事件先触发

可以看到,注册事件监听器前,事件先触发,则该事件会直接被忽略。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var EventEmitter = require('events');

class Man extends EventEmitter {}

var man = new Man();

man.emit('wakeup', 1);

man.on('wakeup', function(index){
    console.log('man has woken up ->' + index);
});

man.emit('wakeup', 2);
// 输出如下:
// man has woken up ->2
复制代码

例子5:异步执行,还是顺序执行

例子很简单,但非常重要。究竟是代码1先执行,还是代码2先执行,这点差异,无论对于我们理解别人的代码,还是自己编写node程序,都非常关键。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

实践证明,代码1先执行了文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var EventEmitter = require('events');

class Man extends EventEmitter {}

var man = new Man();

man.on('wakeup', function(){
    console.log('man has woken up'); // 代码1
});

man.emit('wakeup');

console.log('woman has woken up');  // 代码2

// 输出如下:
// man has woken up
// woman has woken up
复制代码

例子6:移除事件监听器

var EventEmitter = require('events');

function wakeup(){
    console.log('man has woken up');
}

class Man extends EventEmitter {}

var man = new Man();

man.on('wakeup', wakeup);
man.emit('wakeup');

man.removeListener('wakeup', wakeup);
man.emit('wakeup');

// 输出如下:
// man has woken up
复制代码

手写实现EventEmitter

event.js ,使用发布订阅模式实现,原理非常简单,就是在内部用一个对象存储事件和回调的对应关系,并且在合适的时候进行触发。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

let effects = [];

function depend(obj) { // 收集依赖
  effects.push(obj);
}
function notify(key, data) { // 执行依赖
  const fnList = effects.filter(x => x.name === key);
  fnList.forEach(list => list.fn(data))
}

export default {
  $emit(name, data) {
    notify(name, data);
  },
  $on(name, fn) {
    depend({ name, fn });
    return () => { this.$off(name, fn) }; // 为了方便销毁事件,将方法吐出
  },
  $off(name, fn) {
    const fnList = effects.filter(x => x.name === name);
    effects = fnList.filter(x => x.fn !== fn);
  }
}
};
复制代码

调用:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

import bus from "./event";

const busoff = bus.$on('effect', (data) => { // TODO ... data.id ... }) // 注册事件

bus.$emit('effect', { id: xxx }) // 触发事件

busoff() // 事件销毁
复制代码

Stream

在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。譬如,shell通过管道|连接各部分,其输入输出的规范是文本流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

在Node.js中,内置的Stream模块也实现了类似功能,各部分通过.pipe()连接。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Stream提供了以下四种类型的流:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var Stream = require('stream')

var Readable = Stream.Readable
var Writable = Stream.Writable
var Duplex = Stream.Duplex
var Transform = Stream.Transform

复制代码

使用Stream可实现数据的流式处理,如:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var fs = require('fs')
// `fs.createReadStream`创建一个`Readable`对象以读取`bigFile`的内容,并输出到标准输出
// 如果使用`fs.readFile`则可能由于文件过大而失败
fs.createReadStream(bigFile).pipe(process.stdout)
复制代码

Readable

创建可读流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

实例:流式消耗迭代器中的数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

'use strict'
const Readable = require('stream').Readable

class ToReadable extends Readable {
  constructor(iterator) {
    super()
    this.iterator = iterator
  }

  // 子类需要实现该方法
  // 这是生产数据的逻辑
  _read() {
    const res = this.iterator.next()
    if (res.done) {
      // 数据源已枯竭,调用`push(null)`通知流
      return this.push(null)
    }
    setTimeout(() => {
      // 通过`push`方法将数据添加到流中
      this.push(res.value + '\n')
    }, 0)
  }
}

module.exports = ToReadable
复制代码

实际使用时,new ToReadable(iterator)会返回一个可读流,下游可以流式的消耗迭代器中的数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

const iterator = function (limit) {
  return {
    next: function () {
      if (limit--) {
        return { done: false, value: limit + Math.random() }
      }
      return { done: true }
    }
  }
}(1e10)

const readable = new ToReadable(iterator)

// 监听`data`事件,一次获取一个数据
readable.on('data', data => process.stdout.write(data))

// 所有数据均已读完
readable.on('end', () => process.stdout.write('DONE'))
复制代码

执行上述代码,将会有100亿个随机数源源不断地写进标准输出流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

创建可读流时,需要继承Readable,并实现_read方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  • _read方法是从底层系统读取具体数据的逻辑,即生产数据的逻辑。
  • 在_read方法中,通过调用push(data)将数据放入可读流中供下游消耗。
  • 在_read方法中,可以同步调用push(data),也可以异步调用。
  • 当全部数据都生产出来后,必须调用push(null)来结束可读流。
  • 流一旦结束,便不能再调用push(data)添加数据。

可以通过监听data事件的方式消耗可读流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  • 在首次监听其data事件后,readable便会持续不断地调用_read(),通过触发data事件将数据输出。
  • 第一次data事件会在下一个tick中触发,所以,可以安全地将数据输出前的逻辑放在事件监听后(同一个tick中)。
  • 当数据全部被消耗时,会触发end事件。

上面的例子中,process.stdout代表标准输出流,实际是一个可写流。下小节中介绍可写流的用法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Writable

创建可写流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

前面通过继承的方式去创建一类可读流,这种方法也适用于创建一类可写流,只是需要实现的是_write(data, enc, next)方法,而不是_read()方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

有些简单的情况下不需要创建一类流,而只是一个流对象,可以用如下方式去做:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

const Writable = require('stream').Writable

const writable = Writable()
// 实现`_write`方法
// 这是将数据写入底层的逻辑
writable._write = function (data, enc, next) {
  // 将流中的数据写入底层
  process.stdout.write(data.toString().toUpperCase())
  // 写入完成时,调用`next()`方法通知流传入下一个数据
  process.nextTick(next)
}

// 所有数据均已写入底层
writable.on('finish', () => process.stdout.write('DONE'))

// 将一个数据写入流中
writable.write('a' + '\n')
writable.write('b' + '\n')
writable.write('c' + '\n')

// 再无数据写入流时,需要调用`end`方法
writable.end()
复制代码
  • 上游通过调用writable.write(data)将数据写入可写流中。write()方法会调用_write()将data写入底层。
  • 在_write中,当数据成功写入底层后,必须调用next(err)告诉流开始处理下一个数据。
  • next的调用既可以是同步的,也可以是异步的。
  • 上游必须调用writable.end(data)来结束可写流,data是可选的。此后,不能再调用write新增数据。
  • 在end方法调用后,当所有底层的写操作均完成时,会触发finish事件。

Duplex

创建可读可写流。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Duplex实际上就是继承了Readable和Writable的一类流。 所以,一个Duplex对象既可当成可读流来使用(需要实现_read方法),也可当成可写流来使用(需要实现_write方法)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

var Duplex = require('stream').Duplex

var duplex = Duplex()

// 可读端底层读取逻辑
duplex._read = function () {
  this._readNum = this._readNum || 0
  if (this._readNum > 1) {
    this.push(null)
  } else {
    this.push('' + (this._readNum++))
  }
}

// 可写端底层写逻辑
duplex._write = function (buf, enc, next) {
  // a, b
  process.stdout.write('_write ' + buf.toString() + '\n')
  next()
}

// 0, 1
duplex.on('data', data => console.log('ondata', data.toString()))

duplex.write('a')
duplex.write('b')
duplex.write('x')


duplex.end()
复制代码

上面的代码中实现了_read方法,所以可以监听data事件来消耗Duplex产生的数据。 同时,又实现了_write方法,可作为下游去消耗数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

因为它既可读又可写,所以称它有两端:可写端和可读端。 可写端的接口与Writable一致,作为下游来使用;可读端的接口与Readable一致,作为上游来使用。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

Transform

在上面的例子中,可读流中的数据(0, 1)与可写流中的数据(’a’, ‘b’)是隔离开的,但在Transform中可写端写入的数据经变换后会自动添加到可读端。 Tranform继承自Duplex,并已经实现了_read和_write方法,同时要求用户实现一个_transform方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

'use strict'

const Transform = require('stream').Transform

class Rotate extends Transform {
  constructor(n) {
    super()
    // 将字母移动`n`个位置
    this.offset = (n || 13) % 26
  }

  // 将可写端写入的数据变换后添加到可读端
  _transform(buf, enc, next) {
    var res = buf.toString().split('').map(c => {
      var code = c.charCodeAt(0)
      if (c >= 'a' && c <= 'z') {
        code += this.offset
        if (code > 'z'.charCodeAt(0)) {
          code -= 26
        }
      } else if (c >= 'A' && c <= 'Z') {
        code += this.offset
        if (code > 'Z'.charCodeAt(0)) {
          code -= 26
        }
      }
      return String.fromCharCode(code)
    }).join('')

    // 调用push方法将变换后的数据添加到可读端
    this.push(res)
    // 调用next方法准备处理下一个
    next()
  }

}

var transform = new Rotate(3)
transform.on('data', data => process.stdout.write(data))
transform.write('hello, ')
transform.write('world!')
transform.end()

复制代码

数据类型

前面几节的例子中,经常看到调用data.toString()。这个toString()的调用是必需的吗?文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

在shell中,用管道(|)连接上下游。上游输出的是文本流(标准输出流),下游输入的也是文本流(标准输入流)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

对于可读流来说,push(data)时,data只能是String或Buffer类型,而消耗时data事件输出的数据都是Buffer类型。对于可写流来说,write(data)时,data只能是String或Buffer类型,_write(data)调用时传进来的data都是Buffer类型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

也就是说,流中的数据默认情况下都是Buffer类型。产生的数据一放入流中,便转成Buffer被消耗;写入的数据在传给底层写逻辑时,也被转成Buffer类型。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

但每个构造函数都接收一个配置对象,有一个objectMode的选项,一旦设置为true,就能出现“种瓜得瓜,种豆得豆”的效果。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

  1. Readable未设置objectMode时:
const Readable = require('stream').Readable

const readable = Readable()

readable.push('a')
readable.push('b')
readable.push(null)

readable.on('data', data => console.log(data))

复制代码
  1. Readable设置objectMode后:
const Readable = require('stream').Readable

const readable = Readable({ objectMode: true })

readable.push('a')
readable.push('b')
readable.push({})
readable.push(null)

readable.on('data', data => console.log(data))
复制代码

可见,设置objectMode后,push(data)的数据被原样地输出了。此时,可以生产任意类型的数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/26528.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/26528.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定