乐趣区

关于前端:一文读懂NodeJs知识体系和原理浅析

node.js 初探

Node.js 是一个 JS 的服务端运行环境,简略的来说,它是在 JS 语言标准的根底上,封装了一些服务端的运行时对象,让咱们可能简略实现十分多的业务性能。

如果咱们只应用 JS 的话,实际上只是能进行一些简略的逻辑运算。node.js 就是基于 JS 语法减少与操作系统之间的交互。

node.js 的装置

咱们能够应用多种形式来装置 node.js,node.js 实质上也是一种软件,咱们能够应用间接下载二进制安装文件装置,通过零碎包治理进行装置或者通过源码自行编译均可。

一般来讲,对于集体开发的电脑,咱们举荐间接通过 node.js 官网的二进制安装文件来装置。对于打包上线的一些 node.js 环境,也能够通过二进制编译的模式来装置。

装置胜利之后,咱们的 node 命令就会主动退出咱们的零碎环境变量 path 中,咱们就能间接在全局应用 node 命令拜访到咱们方才装置的 node 可执行命令行工具。

node.js 版本切换在个人电脑上,咱们能够装置一些工具,对 node.js 版本进行切换,例如 nvmn

nvm 的全称就是 node version manager,意思就是可能治理 node 版本的一个工具,它提供了一种间接通过 shell 执行的形式来进行装置。简略来说,就是通过将多个 node 版本装置在指定门路,而后通过 nvm 命令切换时,就会切换咱们环境变量中 node 命令指定的理论执行的软件门路。

装置胜利之后,咱们就能在以后的操作系统中应用多个 node.js 版本。

包管理工具 npm

curl -o- https://raw.githubusercontent.com/nvm- sh/nvm/v0.35.3/install.sh | bash

咱们对 npm 应该都比拟相熟了,它是 node.js 内置的一款工具,目标在于装置和公布合乎 node.js 规范的模块,从而实现社区共建的目标凋敝整个社区。

npx 是 npm@5 之后新增的一个命令,它使得咱们能够在不装置模块到以后环境的前提下,应用一些 cli 性能。

例如 npx create-react-app some-repo

node.js 的底层依赖

node.js 的次要依赖子模块有以下内容:

V8 引擎

次要是 JS 语法的解析,有了它能力辨认 JS 语法 \

libuv

c 语言实现的一个高性能异步非阻塞 IO 库,用来实现 node.js 的事件循环

http-parser/llhttp

底层解决 http 申请,解决报文,解析申请包等内容

openssl

解决加密算法,各种框架使用宽泛

zlib

解决压缩等内容 node.js 常⻅内置模块

次要模块

node.js 中最次要的内容,就是实现了一套 CommonJS 的模块化标准,以及内置了一些常⻅的模块。

fs:

文件系统,可能读取写入以后装置零碎环境中硬 盘的数据 \

path:

门路零碎,可能解决门路之间的问题

crypto:

加密相干模块,可能以规范的加密形式对我 们的内容进行加解密

dns:

解决 dns 相干内容,例如咱们能够设置 dns 服 务器等等 \

http:

设置一个 http 服务器,发送 http 申请,监听 响应等等

readline:

读取 stdin 的一行内容,能够读取、减少、删除咱们命令行中的内容 \

os:

操作系统层面的一些 api,例如通知你以后零碎类 型及一些参数

vm:

一个专⻔解决沙箱的虚拟机模块,底层次要来调 用 v8 相干 api 进行代码解析。

V8 引擎:

引擎只是解析层面,具体的下层还有许多具体环境的封装。

Debug & 内存透露

对于浏览器的 JS 代码来说,咱们能够通过断点进行分步调试,每一步打印以后上下文中的变量后果,来定位具体问题呈现在哪一步。

咱们能够借助 VSCode 或者自行打断点的模式,来进行分步 node.js 调试。

对于 JS 内存透露,咱们也能够应用同样的情理,借助工具,打印每次的内存快照,比照得出代码中的问题。

另一种 JS 解析引擎 quickjs

quickjs 是一个 JS 的解析引擎,轻量代码量也不大,与之性能相似的就是 V8 引擎。

他最大的特点就是,十分十分轻量,这点从源码中也能体现,事实上并没有太多的代码,它的次要特点和劣势:

  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 比有什么区别呢?

相同点:
  • deno 也是基于 V8,下层封装一些零碎级别的调用咱们的 deno 利用也能够应用 JS 开发
不同点:
  • deno 基于 rust 和 typescript 开发一些下层模块,所以咱们能够间接在 deno 利用中书写 ts
  • deno 反对从 url 加载模块,同时反对 top level await 等个性

全局对象解析

JavaScript 中有一个非凡的对象,称为全局对象(Global Object),它及其所有属性都能够在程序的任何中央拜访,即全局变量。

在浏览器 JavaScript 中,通常 window 是全局对象,而 Node.js 中的全局对象是 global,所有全局变量(除了 global 自身以外)都是 global 对象的属性。

在 Node.js 咱们能够间接拜访到 global 的属性,而不须要在利用中蕴含它。

全局对象和全局变量

global 最基本的作用是作为全局变量的宿主。依照 ECMAScript 的定义,满足以下条 件的变量是全局变量:

在最外层定义的变量;
全局对象的属性;
隐式定义的变量(未定义间接赋值的变量)。
当你定义一个全局变量时,这个变量同时也会成为全局对象的属性,反之亦然。须要注 意的是,在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于以后模块的,而模块自身不是最外层上下文。

留神:永远应用 var 定义变量以防止引入全局变量,因为全局变量会净化 命名空间,进步代码的耦合危险。

__filename

__filename 示意以后正在执行的脚本的文件名。它将输入文件所在位置的绝对路径,且和命令行参数所指定的文件名不肯定雷同。如果在模块中,返回的值是模块文件的门路。

console.log(__filename);

__dirname

__dirname 示意以后执行脚本所在的目录。

console.log(__dirname);

setTimeout(cb, ms)

setTimeout(cb, ms) 全局函数在指定的毫秒 (ms) 数后执行指定函数(cb)。:setTimeout() 只执行一次指定函数。

返回一个代表定时器的句柄值。

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

clearTimeout、setInterval、clearInterval、console 在 js 中比拟常见,故不做开展。

process

process 是一个全局变量,即 global 对象的属性。

它用于形容以后 Node.js 过程状态的对象,提供了一个与操作系统的简略接口。通常在你写本地命令行程序的时候,少不了要 和它打交道。上面将会介绍 process 对象的一些最罕用的成员办法。

  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 之前, 先看几个基本概念。

背景常识

1. ArrayBuffer

ArrayBuffer 对象用来示意通用的、固定长度的原始二进制数据缓冲区。

ArrayBuffer 不能间接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格局,并通过这些格局来读写缓冲区的内容。

能够把它了解为一块内存, 具体存什么须要其余的申明。

new ArrayBuffer(length)

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

ex. 比方这段代码, 能够执行一下看看输入什么

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

console.log(buffer);
console.log(view);

2. Unit8Array

Uint8Array 数组类型示意一个 8 位无符号整型数组,创立时内容被初始化为 0。
创立完后,能够对象的形式或应用数组下标索引的形式援用数组中的元素。

// 来自长度
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 之上。

ArrayBuffer 自身只是一个 0 和 1 寄存在一行外面的一个汇合,ArrayBuffer 不晓得第一个和第二个元素在数组中该如何调配。

为了能提供上下文,咱们须要将其封装在一个叫做 View 的货色外面。这些在数据上的 View 能够被增加进确定类型的数组,而且咱们有很多种确定类型的数据能够应用。

  1. 总结

总之, ArrayBuffer 基本上表演了一个原生内存的角色.

NodeJs Buffer

Buffer 类以一种更优化、更适宜 Node.js 用例的形式实现了 Uint8Array API.

Buffer 类的实例相似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外调配物理内存。

Buffer 的大小在被创立时确定,且无奈调整。

根本应用

// 创立一个长度为 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 填充)。

尽管这样的设计使得内存的调配十分快,但已调配的内存段可能蕴含潜在的敏感旧数据。应用通过 Buffer.allocUnsafe() 创立的没有被齐全重写内存的 Buffer,在 Buffer 内存可读的状况下,可能泄露它的旧数据。
尽管应用 Buffer.allocUnsafe() 有显著的性能劣势,但必须额定小心,以防止给应用程序引入安全漏洞。

Buffer 与字符编码

Buffer 实例个别用于示意编码字符的序列,比方 UTF-8、UCS2、Base64、或十六进制编码的数据。通过应用显式的字符编码,就能够在 Buffer 实例与一般的 JavaScript 字符串之间进行互相转换。

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 字符串之间进行互相转换。

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

console.log(buf)

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

// 输入 aGVsbG8gd29ybGQ=
console.log(buf.toString('base64'));

Node.js 目前反对的字符编码包含:

  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 内存池。

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 的内存池即可。

  1. 后面简略介绍了 ArrayBuffer 和 Unit8Array 相干的基础知识,而 ArrayBuffer 的利用在 8K 的内存池局部的曾经介绍过了。那接下来当然要轮到 Unit8Array 了,咱们再来回顾一下它的语法:
Uint8Array(length);
Uint8Array(typedArray);
Uint8Array(object);
Uint8Array(buffer [, byteOffset [, length]]);

其实除了 Buffer 类外,还有一个 FastBuffer 类,该类的申明如下:

class FastBuffer extends Uint8Array {constructor(arg1, arg2, arg3) {super(arg1, arg2, arg3);
  }
}

是不是晓得 Uint8Array 用在哪里了,在 FastBuffer 类的构造函数中,通过调用 Uint8Array(buffer [, byteOffset [, length]]) 来创立 Uint8Array 对象。

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

为什么输入了一串数字, 咱们创立的字符串呢? 来看一下源码

/** * 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 对象。

是时候来会一会 fromString() 办法了,它外部实现如下:

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;

所以咱们失去这样的论断

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

事件循环模型

什么是事件循环

事件循环使 Node.js 能够通过将操作转移到零碎内核中来执行非阻塞 I/O 操作(只管 JavaScript 是单线程的)。

因为大多数古代内核都是多线程的,因而它们能够解决在后盾执行的多个操作。当这些操作之一实现时,内核会通知 Node.js,以便能够将适当的回调增加到轮询队列中以最终执行。

Node.js 启动时,它将初始化事件循环,解决提供的输出脚本,这些脚本可能会进行异步 API 调用,调度计时器或调用 process.nextTick,而后开始处理事件循环。

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

每个阶段都有一个要执行的回调 FIFO 队列。只管每个阶段都有其本人的非凡形式,然而通常,当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,而后在该阶段的队列中执行回调,直到队列耗尽或执行回调的最大数量为止。当队列已为空或达到回调限度时,事件循环将移至下一个阶段。

  1. timers:此阶段执行由 setTimeoutsetInterval 设置的回调。
  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,如果没有,则将其洁净地敞开。

各阶段具体解析

timers 计时器阶段

计时器能够在回调前面指定工夫阈值,但这不是咱们心愿其执行的确切工夫。计时器回调将在通过指定的工夫后尽早运行。然而,操作系统调度或其余回调的运行可能会提早它们,即执行的理论工夫不确定。

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 阈值为止。

期待 95 ms 过来时,fs.readFile 实现读取文件,并将须要 10ms 实现的其回调增加到轮询 (poll) 队列并执行。

回调实现后,队列中不再有回调,此时事件循环已达到最早计时器 (timer) 的阈值 (100ms),而后返回到计时器 (timer) 阶段以执行计时器的回调。

pending callbacks 阶段

此阶段执行某些零碎操作的回调,例如 TCP 谬误,平时无需关注。

轮询 poll 阶段

轮询阶段具备两个次要性能:

  1. 计算应该阻塞并 I/O 轮询的工夫
  2. 解决轮询队列 (poll queue) 中的事件

当事件循环进入轮询 (poll) 阶段并且没有任何计时器调度 (timers scheduled) 时,将产生以下两种状况之一:

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

一旦轮询队列 (poll queue) 为空,事件循环将查看哪些计时器 timer 曾经到工夫。如果一个或多个计时器 timer 准备就绪,则事件循环将返回到计时器阶段,以执行这些计时器的回调。

查看阶段 check

此阶段容许在轮询 poll 阶段实现后立刻执行回调。如果轮询 poll 阶段处于闲暇,并且脚本已应用 setImmediate 进入 check 队列,则事件循环可能会进入 check 阶段,而不是在 poll 阶段期待。

setImmediate 实际上是一个非凡的计时器,它在事件循环的独自阶段运行。它应用 libuv API,该 API 打算在轮询阶段实现后执行回调。

通常,在执行代码时,事件循环最终将达到轮询 poll 阶段,在该阶段它将期待传入的连贯,申请等。然而,如果已应用 setImmediate 设置回调并且轮询阶段变为闲暇,则它将将完结并进入 check 阶段,而不是期待轮询事件。

留神:setImmediate为实验性办法,可能不会被批准成为规范,目前只有最新版本的 Internet Explorer 和 Node.js 0.10+ 实现了该办法。

close callbacks 阶段

如果套接字或句柄忽然敞开(例如 socket.destroy),则在此阶段将收回 ‘close’ 事件。否则它将通过 process.nextTick 收回。

setImmediate 和 setTimeout 的区别

setImmediate 和 setTimeout 类似,然而依据调用工夫的不同,它们的行为也不同。

  • setImmediate 设计为在以后轮询 poll 阶段实现后执行脚本。
  • setTimeout 打算在以毫秒为单位的最小阈值过来之后运行脚本。

Tips: 计时器的执行程序将依据调用它们的上下文而有所不同。如果两者都是主模块中调用的,则时序将受到过程性能的限度.

来看两个例子:

  1. 在主模块中执行

    两者的执行程序是不固定的, 可能 timeout 在前, 也可能 immediate 在前

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

    setImmediate 总是先执行

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

问题:那为什么在内部 (比方主代码局部 mainline) 这两者的执行程序不确定呢?

解答:在 主代码 局部执行 setTimeout 设置定时器 (此时还没有写入队列),与 setImmediate 写入 check 队列。

mainline 执行完开始事件循环,第一阶段是 timers,这时候 timers 队列可能为空,也可能有回调;
如果没有那么执行 check 队列的回调,下一轮循环在查看并执行 timers 队列的回调;
如果有就先执行 timers 的回调,再执行 check 阶段的回调。因而这是 timers 的不确定性导致的。

process.nextTick

process.nextTick 从技术上讲不是事件循环的一部分。相同,无论事件循环的以后阶段如何,都将在以后操作实现之后解决 nextTickQueue

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 畛域,微工作是来自以下对象的回调:

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

在主线完结后以及事件循环的每个阶段之后,立刻运行微工作回调。

resolved 的 promise.then 回调像微解决一样执行,就像 process.nextTick 一样。尽管,如果两者都在同一个微工作队列中,则将首先执行 process.nextTick 的回调。

优先级 process.nextTick > promise.then

执行代码看看输入程序

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 等。

模块自身非常简单,API 尽管也不少,但罕用的就那么几个,这里举几个简略例子。

例子 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:同个事件,多个事件监听器

能够看到,事件触发时,事件监听器依照注册的程序执行。

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:注册事件监听器前,事件先触发

能够看到,注册事件监听器前,事件先触发,则该事件会间接被疏忽。

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 程序,都十分要害。

实践证明,代码 1 先执行了

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,应用公布订阅模式实现,原理非常简单,就是在外部用一个对象存储事件和回调的对应关系,并且在适合的时候进行触发。

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);
  }
}
};

调用:

import bus from "./event";

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

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

busoff() // 事件销毁

Stream

在构建较简单的零碎时,通常将其拆解为性能独立的若干局部。这些局部的接口遵循肯定的标准,通过某种形式相连,以共同完成较简单的工作。譬如,shell 通过管道 | 连贯各局部,其输入输出的标准是文本流。

在 Node.js 中,内置的 Stream 模块也实现了相似性能,各局部通过.pipe()连贯。

Stream 提供了以下四种类型的流:

var Stream = require('stream')

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

应用 Stream 可实现数据的流式解决,如:

var fs = require('fs')
// `fs.createReadStream` 创立一个 `Readable` 对象以读取 `bigFile` 的内容,并输入到规范输入
// 如果应用 `fs.readFile` 则可能因为文件过大而失败
fs.createReadStream(bigFile).pipe(process.stdout)

Readable

创立可读流。

实例:流式耗费迭代器中的数据。

'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)会返回一个可读流,上游能够流式的耗费迭代器中的数据。

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 亿个随机数源源不断地写进规范输入流。

创立可读流时,须要继承 Readable,并实现_read 办法。

  • _read 办法是从底层零碎读取具体数据的逻辑,即生产数据的逻辑。
  • 在_read 办法中,通过调用 push(data)将数据放入可读流中供上游耗费。
  • 在_read 办法中,能够同步调用 push(data),也能够异步调用。
  • 当全副数据都生产进去后,必须调用 push(null)来完结可读流。
  • 流一旦完结,便不能再调用 push(data)增加数据。

能够通过监听 data 事件的形式耗费可读流。

  • 在首次监听其 data 事件后,readable 便会继续一直地调用_read(),通过触发 data 事件将数据输入。
  • 第一次 data 事件会在下一个 tick 中触发,所以,能够平安地将数据输入前的逻辑放在事件监听后(同一个 tick 中)。
  • 当数据全副被耗费时,会触发 end 事件。

下面的例子中,process.stdout 代表规范输入流,理论是一个可写流。下大节中介绍可写流的用法。

Writable

创立可写流。

后面通过继承的形式去创立一类可读流,这种办法也实用于创立一类可写流,只是须要实现的是_write(data, enc, next)办法,而不是_read()办法。

有些简略的状况下不须要创立一类流,而只是一个流对象,能够用如下形式去做:

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

创立可读可写流。

Duplex 实际上就是继承了 Readable 和 Writable 的一类流。所以,一个 Duplex 对象既可当成可读流来应用(须要实现_read 办法),也可当成可写流来应用(须要实现_write 办法)。

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 办法,可作为上游去耗费数据。

因为它既可读又可写,所以称它有两端:可写端和可读端。可写端的接口与 Writable 统一,作为上游来应用;可读端的接口与 Readable 统一,作为上游来应用。

Transform

在下面的例子中,可读流中的数据(0, 1)与可写流中的数据(’a’,‘b’)是隔离开的,但在 Transform 中可写端写入的数据经变换后会主动增加到可读端。Tranform 继承自 Duplex,并曾经实现了_read 和_write 办法,同时要求用户实现一个_transform 办法。

'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()的调用是必须的吗?

在 shell 中,用管道(|)连贯上下游。上游输入的是文本流(规范输入流),上游输出的也是文本流(规范输出流)

对于可读流来说,push(data)时,data 只能是 String 或 Buffer 类型,而耗费时 data 事件输入的数据都是 Buffer 类型。对于可写流来说,write(data)时,data 只能是 String 或 Buffer 类型,_write(data)调用时传进来的 data 都是 Buffer 类型。

也就是说,流中的数据默认状况下都是 Buffer 类型。产生的数据一放入流中,便转成 Buffer 被耗费;写入的数据在传给底层写逻辑时,也被转成 Buffer 类型。

但每个构造函数都接管一个配置对象,有一个 objectMode 的选项,一旦设置为 true,就能呈现“种瓜得瓜,种豆得豆”的成果。

  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)的数据被原样地输入了。此时,能够生产任意类型的数据。

退出移动版