欢迎光临散文网 会员登陆 & 注册

6大数据库挖掘7种业务场景的存储更优解天生我材必有用,千金散尽还复来。

2023-03-03 14:28 作者:山观那恭喜囧昂贵的  | 我要投稿


一文读懂NodeJs学问体系和原理浅析
node.js 初探


6大数据库挖掘7种业务场景的存储更优解

download:https://www.zxit666.com/5631/

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 版本停止切换,例如 nvm 和 n。


nvm 的全称就是 node version manager,意义就是可以管理 node 版本的一个工具,它提供了一种直接经过 shell 执行的方式来停止装置。简单来说,就是经过将多个 node 版本装置在指定途径,然后经过 nvm 命令切换时,就会切换我们环境变量中 node 命令指定的实践执行的软件途径。
装置胜利之后,我们就能在当前的操作系统中运用多个 node.js 版本。


我们对 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 引擎。

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

轻量而且易于嵌入: 只需几个C文件,没有外部依赖,一个x86下的简单的“hello world”程序只需180 KiB
具有极低启动时间的快速解释器: 在一台单核的台式PC上,大约在100秒内运转ECMAScript 测试套件156000次的运转时实例完好生命周期在不到300微秒的时间内完成。
简直完好完成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 对象的一些最常用的成员办法。

exit
当进程准备退出时触发。
beforeExit
当 node 清空事情循环,并且没有其他布置时触发这个事情。通常来说,当没有进程布置时 node 退出,但是 ‘beforeExit’ 的监听器能够异步伐用,这样 node 就会继续执行。
uncaughtException
当一个异常冒泡回到事情循环,触发这个事情。假如给异常添加了监视器,默许的操作(打印堆栈跟踪信息并退出)就不会发作。
Signal 事情
当进程接纳到信号时就触发。信号列表详见规范的 POSIX 信号名,如 SIGINT、SIGUSR1 等。
参考 前端进阶面试题细致解答

process.on('exit', function(code) {
// 以下代码永远不会执行
setTimeout(function() {
console.log("该代码不会执行");
}, 0);
console.log('退出码为:', code);
});
console.log("程序执行完毕");
退出的状态码
Uncaught Fatal Exception
有未捕获异常,并且没有被域或 uncaughtException 处置函数处置。
Internal JavaScript Parse Error
JavaScript的源码启动 Node 进程时惹起解析错误。十分稀有,仅会在开发 Node 时才会有。
Internal JavaScript Evaluation Failure
JavaScript 的源码启动 Node 进程,评价时返回函数失败。十分稀有,仅会在开发 Node 时才会有。
Fatal Error
V8 里致命的不可恢复的错误。通常会打印到 stderr ,内容为: FATAL ERROR
Non-function Internal Exception Handler
未捕获异常,内部异常处置函数不知为何设置为on-function,并且不能被调用。
Internal Exception Handler Run-Time Failure
未捕获的异常, 并且异常处置函数处置时本人抛出了异常。例如,假如 process.on(‘uncaughtException’) 或 domain.on(‘error’) 抛出了异常。
Invalid Argument
可能是给了未知的参数,或者给的参数没有值。
Internal JavaScript Run-Time Failure
JavaScript的源码启动 Node 进程时抛出错误,十分稀有,仅会在开发 Node 时才会有。
Invalid Debug Argument
设置了参数–debug 和/或 –debug-brk,但是选择了错误端口。
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 能够被添加进肯定类型的数组,而且我们有很多种肯定类型的数据能够运用。

总结
总之, 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 目前支持的字符编码包括:

'ascii' - 仅支持 7 位 ASCII 数据。假如设置去掉高位的话,这种编码是十分快的。
'utf8' - 多字节编码的 Unicode 字符。许多网页和其他文档格式都运用 UTF-8 。
'utf16le' - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
'ucs2' - 'utf16le' 的别名。
'base64' - Base64 编码。当从字符串创立 Buffer 时,依照 RFC4648 第 5 章的规则,这种编码也将正确地承受 “URL 与文件名平安字母表”。
'latin1' - 一种把 Buffer 编码成一字节编码的字符串的方式(由 IANA 定义在 RFC1345 第 63 页,用作 Latin-1 补充块与 C0/C1 控制码)。
'binary' - 'latin1' 的别名。
'hex' - 将每个字节编码为两个十六进制字符。
Buffer 内存管理
在引见 Buffer 内存管理之前,我们要先来引见一下 Buffer 内部的 8K 内存池。

8K 内存池
在 Node.js 应用程序启动时,为了便当地、高效地运用 Buffer,会创立一个大小为 8K 的内存池。
Buffer.poolSize = 8 * 1024; // 8K
var poolSize, poolOffset, allocPool;
// 创立内存池
function createPool() {
poolSize = Buffer.poolSize;
allocPool = createUnsafeArrayBuffer(poolSize);
poolOffset = 0;
}
createPool();
在 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 的内存池即可。

前面简单引见了 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 对象。

那么如今问题来了,FastBuffer 有什么用?它和 Buffer 类有什么关系?带着这两个问题,我们先来一同剖析下面的简单示例:
const buf = Buffer.from('semlinker');
console.log(buf); //
为什么输出了一串数字, 我们创立的字符串呢? 来看一下源码

/** * 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;
所以我们得到这样的结论

当未设置编码的时分,默许运用 utf8 编码;
当字符串所需字节数大于4KB,则直接停止内存分配;
当字符串所需字节数小于4KB,但超越预分配的 8K 内存池的剩余空间,则重新申请 8K 的内存池;
调用 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 队列。 虽然每个阶段都有其本人的特殊方式,但是通常,当事情循环进入给定阶段时,它将执行该阶段特定的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或执行回调的最大数量为止。 当队列已为空或到达回调限制时,事情循环将移至下一个阶段。

timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
pending callbacks:执行推延到下一个循环迭代的 I/O 回调。
idle, prepare, :仅在内部运用。
poll:取出新完成的 I/O 事情;执行与 I/O 相关的回调(除了关闭回调,计时器调度的回调和 setImmediate 之外,简直一切这些回调) 恰当时,node 将在此处阻塞。
check:在这里调用 setImmediate 回调。
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 阶段
轮询阶段具有两个主要功用:

计算应该阻塞并 I/O 轮询的时间
处置轮询队列 (poll queue) 中的事情
当事情循环进入轮询 (poll) 阶段并且没有任何计时器调度 (timers scheduled) 时,将发作以下两种状况之一:

假如轮询队列 (poll queue) 不为空,则事情循环将遍历其回调队列,使其同步执行,直到队列用尽或到达与系统相关的硬性限制为止。
假如轮询队列为空,则会发作以下两种状况之一:
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: 计时器的执行次第将依据调用它们的上下文而有所不同。 假如两者都是主模块中调用的,则时序将遭到进程性能的限制.

来看两个例子:

在主模块中执行

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

setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
在同一个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 范畴,微任务是来自以下对象的回调:

process.nextTick()
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,就能呈现“种瓜得瓜,种豆得豆”的效果。

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))
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)的数据被原样地输出了。此时,能够消费恣意类型的数据。

6大数据库挖掘7种业务场景的存储更优解天生我材必有用,千金散尽还复来。的评论 (共 条)

分享到微博请遵守国家法律