学习笔记(31):Node.js基础知识(下)

475 阅读29分钟

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字符之间进行转换.
  • BufferString互转案例
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目前支持的字符编码包括
    1. 'ascii'- 仅支持7位ASCII数据,如果设置去掉高位的话,这种编码非常快的.
    2. 'utf8'-多字节编码的unicode字符.许多网页和其他文档格式都使用UTF-8.
    3. 'utf-16le'- 2或4个字节,小字节序编码Unicode字符.支持代理对(U+10000至U+10FFFFF).
    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' - 将每个字节编码为两个十六进制字符.

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对象还是大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总结:
    1. 当未设置编码的时候默认使用utf8编码.
    2. 当字符串所需字节数大于4kb,则直接进行内存分配.
    3. 当字符串所需字节数小于4kb,但超过预分配的8kb内存池空间,则重新申请8kb内存池.
    4. 创建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开始的位置.默认值:0
    • end:新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]]]):拷贝buffer
    • target:要拷贝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:
      1. 监听data事件.
      2. 暂停模式下调用resume方法
      3. 调用pipe将数据发送给可写流
    • 切换到暂停模式的API:
      1. 流动模式下调用pause方法
      2. 流动模式下调用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')
  • 自定义双工流
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:写入字符的编码格式,默认utf8
      • option.mode:文件模式(权限)默认0o666
      • callback(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:写入字符的编码格式,默认utf8
      • option.mode:文件模式(权限)默认0o666
      • callback(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上不支持.默认值:0o777
      • callback(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则忽略,默认值:0
      • options.retryDelay:重试的间隔,如果recursive不为true则忽略.默认值:100
      • options.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就会继续执行.
    • 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计时器阶段此此阶段会执行setTimeoutsetInterval设置的回调
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轮询阶段

  • 轮询阶段具有两个主要功能:
    1. 计算:阻塞轮询``I/O的时间
    2. 处理:轮询队列(poll queue)中的事件.
  • 当事件循环进入轮询阶段,且没有被调度的计时器时,将发生以下两种情况之一:
    1. 如果轮询队列不为空,那么事件循环将遍历访问回调队列并同步执行它们,直到队列用尽,或到达与系统相关的硬限制为止(到底是哪些应限制?).
    2. 如果轮询队列为空,则会发生以下两种情况之一: 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领域,微任务是来自以下对象的回调:
    1. process.nextTick()
    2. 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')