NodeJS常见API
Buffer
1. ArrayBuffer
- (注意:ArrayBuffer不是特指node.js中的Buffer).
- ArrayBuffer 实例对象用来表示固定字节长度的二进制缓存区.
- ArrayBuffer 不能直接操作,需要通过
强类型数组对象
或DataView
对象来操作,它们会将缓冲区中的数据表示为特定格式,并通过这些格式来读写缓冲区的内容. - 创建ArrayBuffer.
new ArrayBuffer(length)
// 参数: length 表示创建的 ArrayBuffer的大小,单位为字节byte.
// 返回值: 一个指定大小的ArrayBuffer对象,其内容被初始化为 0.
// 异常: 如果length 大于 Number.MAX_SAFE_INTEGER (>= 2**53)或者为复数,则抛出RangeError 异常.
- 对比ArrayBuffer与TypedArray
var buffer = new ArrayBuffer(8);
var view = new Int16Array(buffer);
console.log(buffer);
console.log(view);
// 输出结果:
// ArrayBuffer {
// [Uint8Contents]: <00 00 00 00 00 00 00 00>,
// byteLength: 8
// }
// Int16Array(4) [ 0, 0, 0, 0 ]
// 原因 ArrayBuffer 在输出日志时,所展示的是 Uint8Contents 8位无符号内容的视图
// 1Byte = 8bit 1字节 = 8位(比特)
// 在8位视图内单个元素所占字节数为1,所以分为8个元素.
// Int16Array 是16位带符号的强类型数组.
// 1Byte = 8bit 1字节 = 8位(比特)
// 2Byte = 16bit 所以在Int16Array中单个元素所占字节数为2
// 所以需要4个元素.
2. Uint8Array
- 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
// 通过另一个类型数组创建
var x = new Uint8Array([21,31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21
// 拓展
// 强类型数组对象用来解释为单个元素的字节数是不一样的.
// 常量BTTES_PER_ELEMENR表示特定强类型数组中每个元素所占的字节数.
// 与之前那个换算有关
Int8Array.BYTES_PER_ELEMENT; // 1
Uint8Array.BYTES_PER_ELEMENT; // 1
Uint8ClampedArray.BYTES_PER_ELEMENT; // 1
Int16Array.BYTES_PER_ELEMENT; // 2
Uint16Array.BYTES_PER_ELEMENT; // 2
Int32Array.BYTES_PER_ELEMENT; // 4
Uint32Array.BYTES_PER_ELEMENT; // 4
Float32Array.BYTES_PER_ELEMENT; // 4
Float64Array.BYTES_PER_ELEMENT; // 8
3. ArrayBuffer与TypedArray的关系
- ArrayBuffer: 本身只是一个存放二进制
(0跟1)
的集合,并没有规定这些二进制如何分配给数组的元素,所以ArrayBuffer本身无法直接操作,只能通过视图view
来操作. - 因为
视图view
部署了数组的一些接口,这意味着我们可以使用数组的方式来操作ArrayBuffer的内存. - TypedArray:即强类型数组,为ArrayBuffer提供一个
视图(view)
,对其下标进行读写,最终都会反映到所建立的ArrayBuffer之上.
4. NodeJS中的Buffer
Buffer的创建
Buffer
构造器,以一种跟适合Node.js的实现了UintArray API
.Buffer
实例,类似于整数数组,但Buffer
的大小是固定的,且在V8堆外分配物理内存.Buffer.alloc
案例
// 创建一个长度为10,且用0填充的Buffer.
const buf1 = Buffer.alloc(10);
// <Buffer 00 00 00 00 00 00 00 00 00 00>
// 创建一个长度为10,且用0x1填充Buffer.
const buf2 = Buffer.alloc(10,1) // 1为10进制,但是输出为十六进制 如 10 -> 0a
// <Buffer 01 01 01 01 01 01 01 01 01 01>
- 调用
Buffer.allocUnsafe
时,被分配的内存段是未初始化的,这样设计使得内存分配非常快. - 但是这是因为通过
Buffer.allocUnsafe
创建的Buffer的内存没有完全重写,使得如果已分配的内存段中包含敏感旧数据,且Buffer内存为可读的情况下,可能会导致旧数据的泄露 - 从而给程序引入了安全漏洞.
Buffer.allocUnsafe
案例
// 创建一个长度为10,且未初始化的Buffer.
// 这个方法比调用Buffer.alloc()更快.
// 但返回的Buffer实例可能包含旧数据.
// 因此需要fill()或write()重写.
const buf3 = Buffer.allocUnsafe(10);
Buffer.from
案例
// 创建一个包含 [0x1,0x2,0x3] 的Buffer
// 注意数字默认都是16进制的.
const buf4 = Buffer.from([1,2,3]);
// 创建一个包含 UTF-8字节的Buffer
const buf5 = Buffer.from("test");
Buffer的字符编码
- Buffer实例一般用于表示编码字符的序列,比如UTF-8,UCS2,Base64,或十六进制编码数据.
- 通过使用显式的字符编码,就可以在Buffer实例与普通的JavaScript字符之间进行转换.
Buffer
与String
互转案例
const buf = Buffer.from('hello world','ascii');
console.log(buf)
// 输出 68656c6c6f20776f726c64 (十六进制)
console.log(buf.toString('hex'));
// 输出 aGVsbG8gd29ybGQ= (base64)
console.log(buf.toString('base64'))
- node.js目前支持的字符编码包括
- 'ascii'- 仅支持7位ASCII数据,如果设置去掉高位的话,这种编码非常快的.
- 'utf8'-多字节编码的unicode字符.许多网页和其他文档格式都使用UTF-8.
- 'utf-16le'- 2或4个字节,小字节序编码Unicode字符.支持代理对(U+10000至U+10FFFFF).
- 'ucs2'-'utf16le'的别名.
- 'base64'- Base64 编码。当从字符串创建 Buffer 时,按照 RFC4648 第 5 章的规定,这种编码也将正确地接受 "URL 与⽂件名安全字⺟表".
- 'latin1'-一种把Buffer编码写成一字节编码的字符串的方式((由 IANA 定义在 RFC1345 第 63 ⻚,⽤作 Latin-1 补充块与 C0/C1 控制码).
- 'binary' - 'latin1'的别名.
- 'Hex' - 将每个字节编码为两个十六进制字符.
5. Buffer的内存管理
Node的内存管理
- 我们将Node程序运行过程中,此进程所占据的所有内存称为常驻内存.
- 常驻内存由代码区,栈,堆,堆外内存组成.
- 其中代码区,栈,堆是受v8管理的,而堆外内存是不受v8管理的.
图解
常驻内存
______________________|_____________________
| | | |
代码区 栈 堆 堆外内存
|______________|______________| |
受V8管理 不受v8管理
Buffer的内存分配
-
我们知道,Buffer对象的内存分配不是在v8的堆内存中的,而是由node的c++层面实现的内存申请的.
-
原因:
- 由于大对象的存储空间是不确定的,不可能向操作系统申请,这会给操作系统造成压力.
- 所以node在内存的使用上面应用的是c++层面申请内存,在JavaScript中分配内存的策略.
-
介绍一下slab内存分配机制:(次要)
- 采用
预先申请,事后分配
的方式. - 简单来说就是一块申请好的固定大小的内存区域.它有如下三种状态:
- full: 完全分配.
- partial: 部分分配.
- empty: 没有分配.
- 这种机制以
8KB为界限
来决定当前分配的对象是大对象还是小对象,也就是每一个slab的值. - 在JavaScript层面以slab作为单位进行内存的分配.
- 采用
-
总结:
- 在初次加载时就会初始化 1 个 8KB 的内存空间(内存池).
- 根据申请的内存大小分为小Buffer对象和大Buffer对象.
- 对于小Buffer(小于4kb)对象内存分配:判断这个slab剩余空间是否足够容纳.
- 如果足够,就使用剩余空间分配(pool偏移量增加).
- 如果不足,就重新申请一块8KB的内存来分配.(tips:createPool)
- 对于大Buffer(大于4kb)对象内存分配:
- 直接在C++层面分配一个你所需要的大小.(tips:createUnsafeBuffer)
- 对于小Buffer(小于4kb)对象内存分配:判断这个slab剩余空间是否足够容纳.
- 无论是小Buffer对象还是大Buffer对象,内存分配都是在C++层面完成,内存管理是在JavaScript层面,最终还是可以被V8的垃圾回收标记回收,但回收的是Buffer本身,而堆外内存的那些部分职能交给C++;
FastBuffer
- 除了Buffer类外,还有FastBuffer类的
- 我们知道Uint8Array可以这样调用:
Uint8Array(length); // 通过长度
Uint8Array(typedArray); // 通过强类型数组
Uint8Array(object); // 通过对象
Uint8Array(buffer[,byteOffset[,length]]); // 通过Buffer,可选字节偏移量,可选长度.
- FastBuffer的类声明如下:
class FastBuffer extends Uint8Array {
constructor(arg1,arg2,arg3) {
super(arg1,arg2,arg3);
}
...
}
- Buffer.from可以这样调用:
// Buffer.from 属于工厂函数的范畴
Buffer.from(str[,encoding]) // 通过字符串[及编码方式](比类型数组方便就在这里)
Buffer.from(array); // 通过数组
Buffer.from(buffer); // 通过buffer
Buffer.from(arrayBuffer[,byteOffset[,length]]) // 通过Buffer,可选字节偏移量,可选长度.
- FastBuffer总结:
- 当未设置编码的时候默认使用utf8编码.
- 当字符串所需字节数大于4kb,则直接进行内存分配.
- 当字符串所需字节数小于4kb,但超过预分配的8kb内存池空间,则重新申请8kb内存池.
- 创建FastBuffer对象时,会进行数据存储,然后会进行长度校验,更新poolOffset偏移量和字节对齐.
Buffer常用静态方法
Buffer.byteLength(string)
:获取字符串字节长度
console.log(Buffer.byteLength("hello world"))
Buffer.isBuffer(any)
:断言buffer
console.log(Buffer.isBuffer("It's not a buffer"))
console.log(Buffer.isBuffer(Buffer.alloc(10)))
Buffer.concat(Buffer[],byteLength?)
:合并buffer
const buffer1 = Buffer.from("hello");
const buffer2 = Buffer.from("world");
console.log(Buffer.concat([buffer1,buffer2]))
console.log(Buffer.concat([buffer1,buffer2],12))
Buffer常用实例方法
buf.write(sting[,offset[,length]][,encoding])
:将字符串写入buffer
const buf1 = Buffer.alloc(20);
console.log("创建空buffer",buf1);
buf1.write('hello');
console.log(`buf1.write("hello"):写入hello`)
console.log(buf1);
buf1.write("hello",5,3)
console.log(`buf1.write("hello",5,3):偏移五个字节,再写入hello的前三个字节`)
console.log(buf1)
console.log(`输出字符串`)
console.log(buf1.toString())
buf.fill(value[,offset[,end]][,encoding])
:填充buffer- 类比
buf.write
参数:buf.write
:字符串+偏移量+长度+编码方式buf.fill
:任意值+偏移量+结束位置+编码方式
- 类比
buf.write
意义:buf.write
:内容有多少就写入多少,除非规定偏移量和长度buf.fill
:不断重复直到填满容器,除非规定偏移量和结束点
const buf1 = Buffer.alloc(20);
buf1.fill("hello");
console.log(buf1);
const buf2 = Buffer.alloc(20);
buf2.fill("hello",4,6);
console.log(buf2);
buf.length
:buffer长度- 类比静态方法
Buffer.byteLength(string)
:Buffer.byteLength(string)
:输入字符串,返回字节长度.buf.length
:返回buffer实例的字节长度.
const buf1 = Buffer.alloc(10);
console.log(buf1.length);
const buf2 = Buffer.from("eric");
console.log(buf2.length);
console.log(Buffer.byteLength("eric"));
buf.toString([encoding[,start[,end]]])
:将buffer解码成字符串encoding
:使用的字符编码,默认值'utf8'.start
:开始解码的字节索引.默认值0.end
:结束解码的字节索引(不包含)","默认值: buf.length
const buf = Buffer.from('test');
console.log(buf.toString('utf8',1,3));
buf.toJSON()
:返回buffer的JSON格式
const buf = Buffer.from("test");
console.log(buf.toJSON());
// { type: 'Buffer', data: [ 116, 101, 115, 116 ] }
buf.equals(otherBuffer)
:对⽐其它buffer是否具有完全相同的字节
const ABC = Buffer.from('ABC');
const hex414243 = Buffer.from('414243','hex');
const ABCD = Buffer.from('ABCD');
console.log(
ABC,
hex414243,
ABCD
);
console.log(ABC.equals(hex414243))
console.log(ABC.equals(ABCD))
// 输出结果如下:
// <Buffer 41 42 43> <Buffer 41 42 43> <Buffer 41 42 43 44>
// true
// false
buf.slice([start[,end]])
:截取字符串start
:新Buffer
开始的位置.默认值:0end
:新Buffer
结束的位置.
const buf1 = Buffer.from("abcdefghi")
console.log(buf1.slice(2,7).toString());
// 输出结果:
// cdefg
const buf2 = Buffer.from("abcdefghi")
console.log(buf2.toString("utf8",2,7));
// 输出结果:
// cdefg
buf.copy(target[,targetStart[,sourceStart[,sourceEnd]]])
:拷贝buffertarget
:要拷贝Buffer和Uint8Array.targetStart
:目标buffer中开始写入之前要跳过的字节数.默认0.sourceStart
:来源buffer中开始拷贝的索引.默认值0.sourceEnd
:来源buffer中结束拷贝的索引(不包含).默认:buf.length.- source.copy(target):将source中的若干值复制给target
- 返回被复制部分的长度.
- 改变的是target
const buf1 = Buffer.from("abcdefghi");
const buf2 = Buffer.from("test");
console.log(buf1.copy(buf2))
// 输出 4
console.log(buf2.toString)
// 输出 "abcd"
Stream
概念
- 说到
流
,首先我们要知道流数据
, 流数据
就是字节数据
,- 在应用程序中各个对象之间交换与数据传输的时候,总是先将对象中所包含的数据转化为流数据,再通过流进行传输.
- 到达目的对象之后,再将流数据转化为该对象可以使用的数据.
- 所以,流是用来传输流数据的,是一种
传输手段
. - 流的应用:
- http请求和响应
- http中的socket
- 以及压缩加密等
为什么需要流?
- 在
流式传输
中,我们不需要一次性将所有数据加载到内存中,所以内存占用少
. - 也不需要等全部数据传输完成之后才能进行处理,所以
时间利用更高
. - 例如在
传输文件
的场景:- 对于体积较小的文件,我们可以把文件全部写入内存人后,然后写入文件.
- 但是,对于体积较大二进制文件,如音频,视频文件,动则几个GB大小,如果使用这个方法,就很容易造成内存爆仓.
- 这时候我们就需要使用流失传输,读一部分,写一部分,不管文件有多大,只要时间允许,总会处理完成.
node中的流
- 在node中
流
是一个抽象接口,被node中很多对象所实现. - 在node中,默认情况下流处理的数据是
Buffer/String
类型,但是如果设置了objectMode
,则可以让其接收任何JavaScript对象
,此时的流称为对象流
. - node.js中有四种基本的流类型: | 类型 | 中文 | 案例 | | --------- | ---------------------------------------- | ---------------------- | | Readable | 可读流 | fs.createReadStream() | | Writable | 可写流 | fs.createWriteStream() | | Duplex | 可读写流 | net.Socket | | Transform | 在读写过程中可以修改和变换数据的Duplex流 | zlib.createDeflate() |
- 所有流的基类
require('events').EventEmitter
. - 同时我们可以通过
const {Readable,Writable,Duplex,Transform} = require('stream')
加载4种流基类.
可读流
- 可读流是对提供数据的源头的抽象.
- 可读流的例子:
- 标准输入process.stdin
- 子进程标准输出和错误输出child_process.stdout,child_process.stderr
- 文件读取流fs.createReadStream
- 客户端接收的响应response
- 服务端接收的请求request
- 可读流有两种模式,可随时切换:
- 流动模式: 自动读取数据.
- 暂停模式: 暂停读取数据.
- 切换到流动模式的API:
- 监听data事件.
- 暂停模式下调用resume方法
- 调用pipe将数据发送给可写流
- 切换到暂停模式的API:
- 流动模式下调用pause方法
- 流动模式下调用unpipe方法
- 可读流可监听事件:'data','error','end','close','readable'.
- 其他方法: destroy
- 所有的Readable都实现了
stream.Readable
类定义的接口. - 自定义可读流
const Readable = require('stream').Readable;
class CustomReadStream extends Readable {
constructor(source,opt) {
/** 配置项传给基类 */
super(opt)
this.source = source;
this.index = 0;
}
_read(highWaterMark) {
if(this.index === this.source.length) {
this.push(null)
}else{
this.source
/** 截取chunk段 */
.slice(this.index,this.index+highWaterMark)
.forEach(element => {
/** 注意转成字符串 */
this.push(element.toString())
})
}
this.index+=highWaterMark
}
}
const customStream = new CustomReadStream(
// [1,2,3,4,5,6,7,8,9,0],
["A","B","C","D","E","F","G"],
/** 设置缓冲区大小为2 */
{ highWaterMark: 2 }
)
customStream.on('data',chunk => {
/** 如果是console.log每次输出都会占一行 */
process.stdout.write(chunk);
})
fs.createReadStream
的使用案例
const fs = require('fs');
const path = require('path')
const rs = fs.createReadStream(
path.resolve(process.cwd(),'example.md'),
{
flags:'r', // 我们要对文件进行何种操作
mode: 0o666, // 权限位
encoding: 'utf8',// 不穿默认为buffer,显示为字符串
// start: 3, // 从索引3开始读.
// end: 8, // 读到索引8结束
highWaterMark: 3,// 缓冲区大小
}
)
/** 打开文件提示 */
rs.on('open',function() {
process.stdout.write('open the file')
})
/** 显示为utf8字符串 */
rs.setEncoding('utf8');
/** 在数据监听中添加暂停和恢复机制 */
rs.on('data',function (data) {
process.stdout.write(data)
rs.pause(); // 暂停读取和发射data事件
setTimeout(function(){
rs.resume(); // 恢复读取并触发data事件
},2000)
})
/** 如果读取晚间出错了就会触发error事件 */
rs.on('error',function() {
process.stdout.write('error');
})
/** 如果内容读完了,会触发end事件 */
rs.on('end',function(){
process.stdout.write('finish');
})
rs.on('close',function(){
process.stdout.write('close the file')
})
伪代码:
rs = fs.createReadStream(filePath,option)
rs.on("open"|"data"|"error"|"end"|"close")
rs.pause
rs.resume
rs.destroy
可写流
- 可写流是对数据写入'目的地'的一种抽象.
- 可写流的例子:
- 标准输出,错误输出:process.stdout,process.stderr
- 子进程标准输入:child_process.stdin
- 文件写入流:fs.createWriteStream
- 客户端发送的请求:request
- 服务端返回的结果:response
- fs write streams 文件
- 可写流可监听事件: 'drain','error','close','finish','pipe','unpipe'
- 自定义可写流
const Writable = require('stream').Writable;
class CustomWritable extends Writable {
constructor(arr,opt) {
super(opt);
// 将this.arr的指针指向arr对应的地址
this.arr = arr;
}
// 实现_write() 方法
_write(chunk,encoding,callback) {
this.arr.push(chunk.toString());
callback();
}
}
const data = []
const customWritable = new CustomWritable(
data,
{
highWaterMark: 3
}
)
customWritable.write('1')
customWritable.write('2')
customWritable.write('3')
console.log(data)
fs.createWriteStream
使用案例
let fs = require('fs');
let path = require('path')
let ws = fs.createWriteStream(
path.resolve(process.cwd(),'example.md'),
{
flags: 'w',
mode: 0o666,
start:3,
highWaterMark:3 // 默认是16k.
}
)
let flag = ws.write('1');
process.stdout.write(flag.toString('utf8')); // true -> boolean不是标准输出,string 和 buffer 才是标准输出
console.log(flag) // true 可以输出非标准输出
flag = ws.write('2');
process.stdout.write(flag.toString('utf8')); // true
flag = ws.write('3');
process.stdout.write(flag.toString('utf8')); // false -> highWaterMark:3 满3个就返回false
flag = ws.write('4');
process.stdout.write(flag.toString('utf8')); // false
fs.createWriteStream
复杂案例
const fs = require('fs');
const path = require('path');
const ws = fs.createWriteStream(
path.resolve(process.cwd(), 'example'),
{
flags: 'w', // 我们要对文件做何种操作 `w`write
mode: 0o666, // 权限位
start: 0, // 从0开始
highWaterMark: 3 // 缓冲区大小
}
)
let count = 9;
function write() {
let flag = true;
while (flag && count > 0) {
process.stdout.write(`before ${count}\n`)
flag = ws.write(
`${count}`,
'utf8',
/** 回调是异步的 */
((i)=>()=>process.stdout.write(`after ${i}\n`))(count))
count--;
}
}
write();
ws.on('drain', function () {
process.stdout.write('drain\n');
write()
})
ws.on('error', function (error) {
process.stdout.write(`${error.toString()}\n`)
})
/**
before 9
before 8
before 7
after 9
drain
before 6
before 5
before 4
after 8
after 7
after 6
drain
before 3
before 2
before 1
after 5
after 4
after 3
drain
after 2
after 1
*/
// 如果不在需要写入则介意调用end方法关闭写入流
ws.end();
pipe
是一种最简单直接的方法连接两个stream,内部实现了数据传递的整个过程,在开发的时候不需要关注内部数据的流动.pipe
的原理模拟,by fs.highReadStream and fs.highWriteStream
cconst fs = require('fs');
const path = require('path');
const appDirctory = process.cwd()
const ws = fs.createWriteStream(path.resolve(appDirctory, 'pipe2.md'));
const rs = fs.createReadStream(path.resolve(appDirctory, 'pipe1.md'));
rs.on('data', function (chunk) {
/** 1. 将从读取流中读取的数据,传入写入流缓存区,(可实现生产者和消费者速度均衡) */
const flag = ws.write(chunk);
/** 2. 如果写入流缓存区满了,就暂停读取流 */
if (!flag) rs.pause();
})
ws.on('drain', function () {
/** 3. 如果写入流缓存区清空了,就重启读取流 */
rs.resume()
})
rs.on('end', function () {
/** 4. 当读取流读取完毕的时候,我们也结束写入流 */
ws.end();
})
pipe
的用法
const fs = require("fs");
const path = require("path");
const appdirectory = process.cwd();
const from = fs.createReadStream(path.resolve(appdirectory,'pipe1.md'));
const to = fs.createWriteStream(path.resolve(appdirectory,'pipe2.md'));
from.pipe(to);
// setTimeout(()=>{
// console.log('关闭向2.txt写入');
// from.unpipe(to);
// console.log('手动关闭文件流');
// to.end()
// },2000)
- 简单实现
pipe
fs.createReadStream.prototype.pipe = function(dest) {
this.on('data',(data)=>{
const flag = dest.write(data);
if(!flag) this.pause();
})
dest.on('drain',()=>{
this.resume()
})
this.on('end',()=>{
dest.end();
})
}
可读写流(双工流)
- 可读可写流又称双工流.
- 双工流的
可读性操作
和可写性操作
完全独立于彼此,这仅仅是将两个特性组合成一个对象. - 双工流同时实现了Readable和Writable接口.
- 可读可写流的实例:
- TCP sockets: net.createServer(socket)中的clien.
- 可读性: socket.on("data",function(data){...})
- 可写性: socket.write('hello word')
- TCP sockets: net.createServer(socket)中的clien.
- 自定义双工流
const Duplex = require("stream").Duplex;
class CustomDuplex extends Duplex {
constructor(arr, opt) {
super(opt);
this.arr = arr;
this.index = 0;
}
/** 实现_read方法 */
_read(size/** 缓存区大小 */) {
if (this.index >= this.arr.length) {
this.push(null)
} else {
this.arr.slice(this.index, this.index + size).forEach((value) => {
this.push(value.toString());
})
this.index += size;
}
}
/** 实现_write方法 */
_write(chunk, encoding, callback) {
this.arr.push(chunk.toString());
callback()
}
}
const data = [];
const customDuplex = new CustomDuplex(data, { highWaterMark: 3 });
/** 往流中写入数据 */
customDuplex.write("1"/** chunk */);
customDuplex.write("2"/** chunk */);
customDuplex.write("3"/** chunk */);
console.log(data);
/** 从流中读取数据 */
console.log(customDuplex.read(2/** size */).toString())
console.log(customDuplex.read(2/** size */).toString())
转换流
转换流
(Transform Streams),在流式传输中,对输入的内容进行转换之后再输出.转换流
也是一种双工流
,同样实现了Readable和Wrible接口,但是我们在使用的时候只需要实现transform方法即可.- 转换流案例:
- 数据
压缩/解压
模块zlib
流:如createGzip/createGunzip,createDeflate/createInflate.(非流方式zlib.gzip/unzip) - 数据
加密/解密
模块crypto
流:如crypto.createCipher/createDecipher
- 数据
- 自定义转换流:
const Transform = require("stream").Transform;
class customTransform extends Transform {
constructor(opt) {
super(opt);
}
_transform(chunk,encoding,callback) {
/**
* 将转换后的数据输出到可读流
*/
this.push(chunk.toString().toUpperCase());
/**
* 参数1是Error对象
* 参数2如果传入,会被转发到readable.push()
*/
callback();
}
}
let t = new customTransform({highWaterMark: 3});
t.on('data',function(data){
console.log('data',data.toString());
});
// stdin.pipe(t) 表示我们将标准输入写入到我们的转换流t中,此时t是可写流.
// pipe(process.stdout) 表示将转换流t的中的数据读取到标准输出中,此时t是可读流.
process.stdin.pipe(t).pipe(process.stdout);
(拓展)对象流
- 默认情况下,流处理的数据是
Buffer/String
类型的值. - 但是如果设置了
objectMode
属性,则我们可以让流接受任何JavaScript类型
,我们将其称为对象流
const Transform = require("stream").Transform;
const fs = require("fs");
const path = require("path")
const appDirectory = process.cwd()
const rs = fs.createReadStream(path.resolve(appDirectory,"user.json"))
rs.setEncoding("utf8")
const toJSON = new Transform({
readableObjectMode: true,
transform:function(chunk,encoding,callback){
this.push(JSON.parse(chunk));
callback();
}
})
const jsonOut = new Transform({
writableObjectMode: true,
transform:function(chunk,encoding,callback) {
console.log(chunk)
callback();
}
})
rs.pipe(toJSON).pipe(jsonOut)
Read './user.json' of file
Set encoding of ReadStream to 'utf8'
Create transform stream called 'toJSON'
Create transform stream called 'jsonOut'
ReadStream -> to json -> json out
pipe pipe
Events
- Events模块node的核心模块之一,几乎所有常用的node模块都继承了events模块,比如http,fs等;
- 例子1: 为
wakeup事件
,设置一个事件监听器
const EventEmitter = require('events').EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
man.on('wakeup',function(){
console.log('The man has woken up.');
})
man.emit('wakeup');
// 输出如下:
// The man has woken up.
- 例子2: 为
wakeup事件
,设置多个事件监听器
- 当事件触发时,事件监听器会按照注册的顺序执行.
const EventEmitter = require('events').EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
man.on('wakeup',function(){
console.log('The man has woken up.');
})
man.on('wakeup',function(){
console.log('The man has woken up again.');
})
man.emit('wakeup');
// 输出如下:
// The man has woken up.
// The man has woken up again.
- 例子3: 注册只运行一次的事件监听器.(
once
)
const EventEmitter = require('events').EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
man.on('wakeup',function(){
console.log('The man has woken up.');
})
man.once('wakeup',function(){
console.log('The man has woken up again.');
})
man.emit('wakeup');
// 输出如下:
// The man has woken up.
// The man has woken up again.
man.emit('wakeup');
// 输出如下:
// man has woken up
- 例子4: 在注册事件监听之前,如果先触发事件,则该事件会被忽略.
const EventEmitter = require("events").EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
man.emit('wakeup',1)
man.on('wakeup',function(index){
console.log('The man has woken up ->'+index)
})
man.emit('wakeup',2)
// 输出如下:
// The man has woken up -> 2'
- 例子5: 证明EventEmitter是
顺序执行
,而不是异步执行
const EventEmitter = require("events").EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
man.on('wakeup',function(){
console.log('The man has woken up');
})
man.emit('wakeup')
console.log('The woman has woken up');
// 输出如下:
// The man has woken up
// The woman has woken up
// 结论:
// 顺序执行
- 例子6: 移除事件监听器
const EventEmitter = require("events").EventEmitter;
class Man extends EventEmitter {}
const man = new Man();
function wakeup () {
console.log('The man has woken up')
}
man.on('wakeup',wakeup);
man.emit('wakeup')
// 输出如下:
// The man has woken up
man.removeListener('wakeup',wakeup);
man.emit('wakeup')
// 无输出
- 手动实现
EventEmitter
/**
* 辅助函数:
* 在参数中带有监听器的时候我们需要校验监听器listener
*/
function checkListener(listener) {
if(typeof listener !== "function") {
throw TypeError("'listener must be a function.")
}
}
/**
* 事件监听构造函数
*/
class EventEmitter {
constructor() {
this._events = {}
}
addListener(eventName,listener) {
checkListener(listener)
if(!this._events[eventName]) {
this._events[eventName] = []
}
this._events[eventName].push(listener);
}
on(eventName,listener) {
this.addListener(eventName,listener);
}
emit(eventName,...args) {
const listeners = this._events[eventName];
if(!listeners) return;
listeners.forEach(fn=>fn.apply(this,args));
}
removeListener(eventName,listener) {
checkListener(listener);
const listeners = this._events[eventName];
if(!listeners) return false;
const index = listeners.findIndex(item => item === listener);
if(index === -1) return false;
listeners.splice(index,1)
}
off(eventName,listener) {
this.removeListener(eventName,listener)
}
removeAllListeners(eventName) {
if(this._events[eventName]) {
delete this._events[eventName];
}
}
once(eventName,listener) {
checkListener(listener)
/** 包装成调用自毁的函数 */
const wrap = (...args) => {
listener.apply(this,args);
this.removeListener(eventName,wrap);
}
this.addListener(eventName,wrap);
}
}
主要方法罗列
EventEmitter
|__ addListener on once
|__ emit
|__ removeListener off removeAllListener
fs
概念
- node文件系统常用模块
fs相关API
fs.readFile(path[,option],callback)
:读取文件内容path
:文件路径encoding?
:编码方式callback(err,data)
:回调函数
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.readFile(
path.resolve(appDirectory,"example.md"),
"utf8",
(err,data) => {
if(err) throw err;
process.stdout.write(data)
}
)
fs.writeFile(file,data[,option],callback)
:写入文件内容file
:文件名或文件描述符data
:写入的数据option
:option.encoding
:写入字符的编码格式,默认utf8option.mode
:文件模式(权限)默认0o666callback(err)
:回调函数
- 注意:
fs.writeFile
,默认是全部替换的. - 参阅支持的文件系统标志.默认值'w'
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
const data = "It is a test."
fs.writeFile(
path.resolve(appDirectory,"example.md"),
data,
(err)=>{
if(err) throw err;
console.log("写入成功")
}
)
fs.appendFile(file,data[,option],callback)
:追加文件内容file
:文件名或文件描述符data
:追加的数据option
:option.encoding
:写入字符的编码格式,默认utf8option.mode
:文件模式(权限)默认0o666callback(err)
:回调函数
- 注意:
fs.appendFile
:默认是在尾部追加 - 参阅支持的文件系统标志.默认值'a'
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
const data = "It is a content which append"
fs.appendFile(
path.resolve(appDirectory,"example.md"),
data,
(err)=>{
if(err) throw err;
console.log("追加成功")
}
)
fs.stat(path[,option],callback)
:判断文件状态(包括它是不是文件夹)path
:文件路径options
:options.bigint
:返回的fs.stat对象中的数值是否为bigint型,默认值false
callback(err,stats)
:回调函数
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.stat(
path.resolve(appDirectory,"example.md"),
(err,stats)=>{
if(err) throw err;
console.log(stats)
/** 这两个是无法枚举的而且是函数我们直接打印出来 */
console.log(stats.isFile())
console.log(stats.isDirectory())
}
)
// Stats {
// dev: 4004941460,
// mode: 33206,
// nlink: 1,
// uid: 0,
// gid: 0,
// rdev: 0,
// blksize: undefined,
// ino: 1688849861160504,
// size: 56,
// blocks: undefined,
// atimeMs: 1618224089682.408,
// mtimeMs: 1618224089682.408,
// ctimeMs: 1618224089682.408,
// birthtimeMs: 1616552557350.4214,
// atime: 2021-04-12T10:41:29.682Z,
// mtime: 2021-04-12T10:41:29.682Z,
// ctime: 2021-04-12T10:41:29.682Z,
// birthtime: 2021-03-24T02:22:37.350Z }
// true
// false
fs.rename(oldPath,newPath,callback)
:重命名文件oldPath
:旧文件路径newPath
:新文件路径callback(err)
:回调函数
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.rename(
path.resolve(appDirectory,"rename.md"),
path.resolve(appDirectory,"example.md"),
(err)=>{
if(err) throw err;
console.log('重命名文件')
}
)
fs.unlink(path,callback)
:删除文件path
: 文件路径callback(err)
:回调函数
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.unlink(
path.resolve(appDirectory,"example.md"),
(err)=>{
if(err) throw err;
console.log('删除成功')
}
)
fs.mkdir(path[,options],callback)
:创建文件path
:路径options
:options.recursive
:是否递归创建.默认值false(注意低版本不支持递归创建)options.mode
:文件模式(权限)Windows上不支持.默认值:0o777callback(err)
:回调函数
/** 非递归创建 */
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.mkdir(
path.resolve(appDirectory,"example"),
(err)=> {
if(err) throw err;
console.log("文件夹创建成功")
}
)
/** 递归创建 */
fs.mkdir(
path.resolve(appDirectory,"a/b"),
{
recursive:true
},
err => {
if(err) throw err;
console.log("递归创建文件夹成功");
}
)
fs.readdir(path[,options],callback)
:读取文件夹path
:路径options
:options.encoding
:决定读取之后返回的内容的编码类型,默认'utf8'.options.withFileTypes
:默认值false.
callback(err,files<string[]>|<buffre[]>|<fs.Dirent[]>)
;
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.readdir(
path.resolve(appDirectory,"b"),
{
encoding:'buffer', // 设置为buffer,files返回文件名为buffer对象
withFileTypes: true // 加上文件类型
},
(err,files) => {
if(err) throw err;
console.log(files);
}
)
// [
// Dirent { name: <Buffer 63>, [Symbol(type)]: 2 }
// ]
fs.rmdir(path[,options],callback)
:删除文件夹path
:路径options
:options.maxRetries
: 重试次数.出现错误EBUSY,EMFILE,ENFILE,ENOTEMPTY或者EPERM,每一个重试会根据设置的重试间隔重试操作.如果recursive不为true则忽略,默认值:0options.retryDelay
:重试的间隔,如果recursive不为true则忽略.默认值:100options.recursive
:如果为true,则执行递归的目录删除.在递归模式中,如果path不存在则会报错
callback(err)
:回调函数
const fs = require("fs");
const path = require("path");
const appDirectory = process.cwd();
fs.rmdir(
path.resolve(appDirectory,"b"),
{
recursive: true // 我们一般用的就是递归删除,文件夹内是否存在内容,对于用户来说是黑匣的
},
err => {
if(err) throw err;
console.log("文件删除成功")
}
)
/** 错误代码意义:https://blog.csdn.net/a8039974/article/details/25830705 */
node.js文件系统操作相关库
- 监听文件变化chokidar
- 安装
npm install chokidar --save-dev
const chokidar = require("chokidar");
chokidar
.watch(
process.cwd(),
{
ignorad: './node_modules'
}
)
.on(
'all',
(event,_path) => {
console.log('监听变化',event,_path)
}
)
path
path.basename(path[,exit])
:返回path
的最后一部分路径
const path = require("path");
console.log(
path.basename(
path.resolve(process.cwd(),"example.md"),
".md"
)
)
// example
path.dirname(path)
:返回文件所在目录
const path = require("path");
console.log(
path.dirname(
path.resolve(process.cwd(),"example.md")
)
)
// D:\parent
path.extname(path)
:返回文件扩展名
const path = require("path");
console.log(
path.extname(
path.resolve(process.cwd(),"example.md")
)
)
// .md
path.join([...paths])
:返回path
的路径拼接
const path = require("path");
console.log(
path.join("/nodejs/","/example.md")
)
// \nodejs\example.md
// 拼接时去掉多余的斜杠
path.normalize(path)
:规范化路径
const path = require("path");
console.log(
path.normalize("/nodejs/example2.md/../example.md")
)
// \nodejs\example.md
// 格式化绕来绕去的表达方式
path.resolve([..paths])
:将路径解析为绝对路径- 类比
path.join()
:path.resolve()
:1.将路径解析为绝对路径.2.但不解决取出多余斜杠问题.path.join()
:只解决多余斜杠问题.
- 类比
const path = require("path");
console.log(
path.resolve("./example.md")
)
// D:\parent\example.md
console.log(
path.resolve(process.cwd(),"/example.md")
)
// D:\example.md 解析错误 -> 变成了顶层目录
console.log(
path.resolve(process.cwd(),"example.md")
)
// D:\parent\example.md
path.parse(path)
:返回一个对象,包含path的属性
const path = require('path');
const pathObj = path.parse('/nodejs/test/index.js');
console.log(pathObj)
// {
// root: '/',
// dir: '/nodejs/test',
// base: 'index.js',
// ext: '.js',
// name: 'index'
// }
path.format(pathObject)
:将路径对象转换成路径
const path = require('path');
const pathObj = path.parse('/nodejs/test/index.js');
console.log(pathObj)
// {
// root: '/',
// dir: '/nodejs/test',
// base: 'index.js',
// ext: '.js',
// name: 'index'
// }
console.log(path.format(pathObj));
// /nodejs/test\index.js (发现斜杠有些奇怪)
path.sep
:返回系统特定的路径片段分隔符
const path = require("path")
console.log(path.sep)
// \
path.win32
:可以实现访问widows的path方法
util(常用工具)
util.callbackify(original)
:传入一个返回值为promise的函数,构造成异常优先
回调风格的函数.
const util = require('util');
async function hello() {
return 'hello world'
}
let helloCb = util.callbackify(hello);
helloCb((err,res)=>{
if(err) throw err;
console.log(res)
})
util.promisify(original)
:传入一个异常优先
回调风格的函数,构造成promise函数.
const fs = require("fs");
const util = require("util");
const path = require("path");
const stat = util.promisify(fs.stat);
stat(
path.resolve(process.cwd(),"example.md")
)
.then(data=>{
console.log("获取文件状态成功",data);
})
.catch(error=>{
console.error("获取文件状态失败",error);
})
// 获取文件状态成功 Stats {
// dev: 109512952,
// mode: 33206,
// nlink: 1,
// uid: 0,
// gid: 0,
// rdev: 0,
// blksize: 4096,
// ino: 140737488355889780,
// size: 7,
// blocks: 0,
// atimeMs: 1618245914082.411,
// mtimeMs: 1617905296801.911,
// ctimeMs: 1617905296801.911,
// birthtimeMs: 1617904969580.9304,
// atime: 2021-04-12T16:45:14.082Z,
// mtime: 2021-04-08T18:08:16.802Z,
// ctime: 2021-04-08T18:08:16.802Z,
// birthtime: 2021-04-08T18:02:49.581Z
// }
const fs = require("fs");
const util = require("util");
const path = require("path");
const stat = util.promisify(fs.stat);
(async function statFn() {
try {
const data = await stat(path.resolve(process.cwd(),"example.md"))
console.log("查看文件状态成功",data)
}
catch (error) {
console.error("查看文件状态失败",error)
}
})()
// 获取文件状态成功 Stats {
// dev: 109512952,
// mode: 33206,
// nlink: 1,
// uid: 0,
// gid: 0,
// rdev: 0,
// blksize: 4096,
// ino: 140737488355889780,
// size: 7,
// blocks: 0,
// atimeMs: 1618245914082.411,
// mtimeMs: 1617905296801.911,
// ctimeMs: 1617905296801.911,
// birthtimeMs: 1617904969580.9304,
// atime: 2021-04-12T16:45:14.082Z,
// mtime: 2021-04-08T18:08:16.802Z,
// ctime: 2021-04-08T18:08:16.802Z,
// birthtime: 2021-04-08T18:02:49.581Z
// }
util.types.isDate(value)
:判断是否为Date数据
const util = require("util");
console.log(
util.types.isDate(new Date())
)
- 另外,
lodash
作为一个一致性,模块化,高性能的JavaScript实用工具,也十分推荐.
npm install lodash --save
全局对象
- JavaScript中有一个特殊的对象,称为
全局对象(Global Object)
.- 我们可以在程序的任何地方访问全局对象和它的属性.
- 我们将
全局对象
的属性称为全局变量
.
- 例如:
- 在浏览器中,通常以window作为全局对象
- 在nodejs中,以global作为全局对象
全局对象和全局变量
- 全局对象的
最根本的作用
是作为全局变量的宿主
. - 按照ECMAScript的定义,满足以下条件之一的变量就是全局变量:
- (前身为欧洲计算机制造商协会,European Computer Manufacturers Association Script)
- 全局对象的属性;
- 在最外层定义的变量;
- 隐式定义的变量(未定义直接赋值的变量);
- 需要注意的是:
- 虽说:当你在最外层定义一个变量时,这个变量就会就会变成全局对象的属性.
- 但是在node.js中,你不可能在最外层定义变量,因为所有的代码都是属于当前模块的,而模块本身不是最外层上下文.
- 使用原则:
- 避免未定义直接赋值变量,从而导致全局变量的引入.
- 因为滥用全局变量会
污染命名空间
和加大代码耦合风险
.
__filename
__filename
表示当前执行脚本
的文件名.- 换言之就是,当前模块的绝对路径,属于模块内部的变量
- 同
path.filename()
.
__dirname
__dirname
表示当前执行脚本
所在的目录.- 换言之就是,当前模块所在目录的绝对路径,属于模块内部的变量
- 同
path.dirname()
process.cwd()
表示当前执行node命令
所在的工作目录.
setTimeout(cb,ms)
clearTimeout
setInterval
clearInterval
console
processs
process
是一个全局变量(即global对象的属性).- 它用于
描述当前node.js进程状态
的对象,提供一个与操作系统的简单接口. - 以下是
process.on
可以监听的一些事件:exit
: 当前进程准备退出时触发.beforeExit
: 当进程清空事件循环,并且没有其他安排的时候触发.- 通常来说,当进程没有其他安排的时候,node就会退出,但是,
beforeExit
的监听器可以异步调用,这样node就会继续执行.
- 通常来说,当进程没有其他安排的时候,node就会退出,但是,
uncaughtException
: 当一个异常冒泡到事件循环时触发.Signal事件
: 当进程收到信号时触发.- 信号列表详见POSIX信号名,如SIGINT,SIGUSR1等
process.on('exit',function(code){
// 以下代码永远不会执行
setTimeout(function() {
console.log('该代码不会执行')
},0)
console.log('退出码为:',code)
})
console.log('程序执行结束');
- 退出状态码: | 状态码 | 英文描述 | 中文描述 | |---|---|---| |1 |Uncaught Fatal Exception | 有未捕获异常,并且没有被域或 uncaughtException 处理函数处理。| |3 |Internal JavaScript Parse Error | JavaScript的源码启动 Node 进程时引起解析错误。⾮常罕⻅,仅会在开发 Node 时才会有。| |4 |Internal JavaScript Evaluation Failure | JavaScript 的源码启动 Node 进程,评估时返回函数失败。⾮常罕⻅,仅会在开发 Node 时才会有。| |5 |Fatal Error| V8 ⾥致命的不可恢复的错误。通常会打印到 stderr ,内容为: FATAL ERROR | |6 |Non-function Internal Exception Handler | 未捕获异常,内部异常处理函数不知为何设置为on-function,并且不能被调⽤。| |7 |Internal Exception Handler Run-Time Failure | 未捕获的异常, 并且异常处理函数处理时⾃⼰抛出了异常。例如,如果process.on(‘uncaughtException’) 或 domain.on(‘error’) 抛出了异常。| |9 |Invalid Argument | 可能是给了未知的参数,或者给的参数没有值。 | |10 |Internal JavaScript Run-Time Failure | JavaScript的源码启动 Node 进程时抛出错误,⾮常罕⻅,仅会在开发 Node 时才会有。| |12 |Invalid Debug Argument | 设置了参数–debug 和/或 –debug-brk,但是选择了错误端⼝. | |>128 |Signal Exits | 如果 Node 接收到致命信号,⽐如SIGKILL 或 SIGHUP,那么退出代码就是128 加信号代码。这是标准的 Unix 做法,退出信号代码放在⾼位。 |
process.stdout.write
:标准输出终端
process.stdout.write('hello world!'+"\n");
process.argv
:返回启动进程时
所输入的命令行参数
构成的数组.
console.log(process.argv);
node scripts.js
# [
# 'D:\\nodejs\\node.exe',
# 'D:\\my_frontend_files\\systematization\\jscripts.js'
# ]
process.execPath
:返回启动进程时
所使用的命令行工具.
console.log(process.execPath);
node scripts.js
# 'D:\nodejs\node.exe'
process.platform
:返回平台信息
.
console.log(process.platform);
# 'win32'
模块中this的指向问题
this
指向exports
- 证明:
console.log(this);
module.exports.foo = 5;
console.log(this);
// {}
// { foo: 5 }
// 也就是说this指向exports
node.js 事件循环模型
背景
- 在传统的web服务中,大都使用
多线程机制
来解决并发问题. - 原因是I/O事件会阻塞线程,阻塞就意味着需要等待.
- 而node.js的设计采用了
单线程机制
,即每个node.js进程只有一个主线程来执行代码. - 所以node.js采用实现循环机制将阻塞的I/O操作交给线程池中的某个线程去完成.
- 主线程本身只负责不断调度,并没有执行真正的I/O操作.
事件循环
事件循环
是Node.js处理非阻塞I/O操作
的机制.- 我们知道,Node.js采用了
单线程机制
,也就是说,每个Node.js进程
只有一个主线程来执行代码. - 那么
事件循环
允许Node.js
通过将操作转移到到系统内核中
这种方式,去执行I/O操作. - 又因为大多数内核都是多线程的,因此它们可以处理后台执行的多个操作.
- 当其中一个操作完成时,内核会通知Node.js,以便将回调添加到
轮询队列
中来最终执行.
事件循环运行过程简单介绍
- 当Node.js启动时,它将初始化事件循环:
- 处理提供的输入脚本,这些脚本可能会进行API,计时器或process.nextTick的调用.
- 然后开始处理事件循环:
- 每个阶段都有一个FIFO队列来执行回调.
- 虽然每个阶段都是特殊的,但通常情况下,当事件循环进入特定的阶段的时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行.
- 当该队列用尽或到达回调限制时,事件循环将移动到下一个阶段.
- 另外,在每次事件循环运行之间,node.js会检查它是否正在等待任何异步I/O或timers,如果没有,则将完全关闭.
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
各个阶段预览
阶段 | 中文 | 描述 |
---|---|---|
timers | 计时器阶段 | 此此阶段会执行setTimeout 和setInterval 设置的回调 |
pending callbacks | 待定回调阶段 | 此阶段执行某些系统操作的回调,如TCP错误(平时无需关注) |
idle,prepare | 闲置,预备阶段 | 仅在内部使用 |
poll | 轮询阶段 | 取出新完成的I/O事件;执行与I/O相关的回调,(除了计时器,setImmdiate,关闭回调的调度之外)其余情况,在适当的时候,node将在此处阻塞 |
check | 检测阶段 | setImmediate() 回调函数在这里执行 |
close callbacks | 关闭时回调阶段 | 执行一些关闭回调,如socket.on("close",...) |
各个阶段详细解析
timers(计时器阶段)
- 我们知道,
计时器
可以在回调后面指定时间阈值,但这并不是它执行的确切时间. - 因为系统调度或者其他回调的运行可能会延迟它们.
计时器回调
只是在指定时间之后尽早的运行.- 解析以下代码:
- 当事件循环进入
轮询阶段
时,它有一个空队列(fs.readFile尚未完成). - 等待95ms过去时,fs.readFile完成读取文件,并将需要10ms完成的其回调,然后添加到轮询队列中执行.
- 回调完成后,队列中不再有回调.
- 此时事件循环检测到计时器(timer)的阈值(100ms)已经达到,然后返回到
计时器阶段
以执行计时器的回调. - 所以,计时器的执行回调的实际延迟时间为105ms.
- 当事件循环进入
const fs = require("fs");
function someAsyncOperation(callback) {
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)
someAsyncOperation(()=>{
const startCallback = Date.now();
while(Date.now() - startCallback < 10) {
}
})
pending callbacks待定回调阶段
- 此阶段执行某些系统操作的回调,例如TCP错误.平时无需关注.
poll轮询阶段
- 轮询阶段具有两个主要功能:
- 计算:
阻塞
和轮询``I/O
的时间 - 处理:
轮询队列
(poll queue)中的事件.
- 计算:
- 当事件循环进入
轮询阶段
,且没有被调度的计时器
时,将发生以下两种情况之一:- 如果
轮询队列
不为空,那么事件循环
将遍历访问回调队列
并同步执行它们,直到队列用尽,或到达与系统相关的硬限制为止(到底是哪些应限制?). - 如果
轮询队列
为空,则会发生以下两种情况之一: 2.1 如果脚本
已经被setImmediate()
调度过,则事件循环
将结束轮询阶段
,并继续检查阶段
,以执行那些被调度的脚本
. 2.2 如果脚本
没有被setImmediate()
调度过,则事件循环
将等待回调被添加如队列中
后,立即执行它们.
- 如果
- 一旦
轮询队列
为空,事件循环
将检查:有哪些计时器已经到达时间阈值?
.如果有一个或多个计时器
准备就绪,则事件循环
将返回到计时器阶段
,以执行这些计时器的回调.
check检查阶段
- 如果
轮询阶段
处于空闲,并且脚本使用了setImmediate()
后被排入check队列中,则事件循环
会进入检查阶段
,而不是在轮询阶段
等待. - 此阶段允许在轮询poll阶段完成后立即执行回调.
setImmediate()
:- 事实上是一个
特殊的计时器
,它在事件循环
的单独阶段
(检查阶段)运行. - 它使用
libuv API
,该API在轮询阶段
完成后执行回调.
- 事实上是一个
- 通常,在执行代码时,事件循环最终达到轮询阶段,该阶段将等待传入的连接,请求等.
- 但是,如果已经使用了
setImmdiate()
设置回调,并且轮询阶段
为空闲,则它将结束并进入检查阶段
,而不是等待轮询事件.
close callbacks关闭回调函数阶段
- 如果套接字或句柄突然关闭(例如
socket.destroy()
),则在此阶段将发出'close'
事件。 - 否则它将通过
process.nextTick()
发出。
setImmediate和setTimeout的区别
setImmediate()
和setTimeout()
相似,但是它们调用的时机不同.setImmediate()
设计为在当前轮询阶段
完成后执行脚本.setTimeout()
计划在以毫秒为单位的最小阈值过去之后
运行脚本.- 调用顺序:
计时器
的执行顺序
将根据调用它们的上下文而异.- 在
主模块内
调用时:- 顺序将会受到进程性能的限制,而导致两个顺序不固定.(这可能是受到计算机上其他正在运行的应用程序影响).
- 在
I/O回调(也就是非主模块)内
调用时:setImmdiate()
总是先执行.
- 为什么在
主模块内
这两者的执行顺序不确定呢?- 主代码部分:执行setTimeout时,
- 问题为什么在外部(比如主代码部分mainline)这两者执行顺序不确定呢?
- 解答:
- 首先,主代码部分会执行
setTimeout()
设置定时器,此时由于setTimeout()
还没有达到时间阈值,所以还没有被写入timers队列
. - 但是
setImmediate()
被执行之后,会被立即写入check队列
. - 主代码执行完,开始
事件循环
,第一阶段是timers, - 这时候
timer队列
可能为空,也可能有回调.- 如果
timer队列
没有回调,那么执行check队列
的回调,下一轮循环再检查并执行timers队列
的回调; - 如果
timer队列
有回调,就先执行计时器阶段
的回调,在执行check阶段
的回调.因此这是由于计时器
的不确定性导致的.
- 如果
process.nextTick
-
process.nextTick()
从技术上讲不是事件循环
的一部分.相反,无论事件循环
当前阶段如何,都将在当前操作完成后处理nextTick队列
. -
process.nextTick()
和setImmediate()
的区别:process.nextTick()
:在同一阶段立即执行.setImmediate()
:在事件循环
接下来的迭代或tick
上触发.
-
nextTick在事件循环中的位置
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
| | idle, prepare │
| └─────────────┬─────────────┘
| nextTickQueue nextTickQueue
| ┌─────────────┴─────────────┐
| │ poll │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
│ │ check │
│ └─────────────┬─────────────┘
│ nextTickQueue
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Microtasks 微任务
- 在node领域,
微任务
是来自以下对象的回调:process.nextTick()
then()
- 时机:在
主线程结束后
以及事件循环的每个阶段之后
,立即运行微任务回调
. process.nextTick()
与promise.then
的优先级:- 如果两个都在
同一个微任务队列
中,则会首先执行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')