Node.js——I/O流操作

I/O流操作

I/O流,即输入(input)、输出(output)流,它提供了一种向存储设备写入数据和从存储设备读取数据的方式,它是Node.js中执行读写文件操作的一种非常重要的方式。

1、流简介

1.1、流的基本概念

程序中的流是一个抽象概念,当程序需要从某个数据源读取数据时,就会开启一个数据流,数据源可以是文件、内存或者网络等,而当程序将数据写入某个数据源时,也会开启一个数据流,而数据源的目的地也可以是文件、内存或者网络等。

以文件流为例,当需要读取一个文件时,如果使用fs模块的readFile()方法读取,程序会将该文件的内容视为一个整体,为其分配缓存区并一次性将内容读取到缓存区中,在这期间,Node.js将不能执行任何其他处理,这就可能导致一个问题,即如果文件很大,会耗费较多的时间。

如果使用文件流读取文件,则可以将文件一部分一部分地读取,这样可以保证效率,并且不会占用太大的内存。

1.1.1、流的基本类型

Node.js中的流有4种基本类型,分别如下。

  • Readable:可读流。
  • Writable:可写流。
  • Duplex:可读可写流(也称双工流)。
  • Transform:转换流,表示在读写过程中可以修改和变换数据的Duplex流。
1.1.2、流模块的引用

使用Node.js中的流之前,首先要引用stream模块,代码如下:

js 复制代码
const stream = require('stream');

1.2、Buffer

Buffer就是开辟的一块内存区域,Buffer的大小就是开辟的内存区域的大小。在流中,Buffer的大小取决于传入流构造函数的highWaterMark选项参数,该参数指定了字节总数或者对象总数,当可读缓冲区的总大小达到highWaterMark指定的阈值时,流会暂时停止从数据源读取数据,直到当前缓冲区的数据被释放;如果要获取缓冲区中存储的数据,需要使用writable.writableBuffer或readable.readableBuffer。

2、可读流的使用

2.1、流的读取模式与状态

2.1.1、可读流的读取模式

可读流有两种读取模式,即流动(flowing)模式和暂停(paused)模式。

所有的可读流都是以暂停模式开始,当流处于暂停模式时,可以通过read()方法从流中按需读取数据。

流动模式指的是一旦开始读取文件,会按照highWaterMark的值按次读取,直到读取完为止。当流处于流动模式时,因为数据是持续变化的,所以需要使用监听事件来处理它。

流的暂停模式和流动模式是可以互相切换的,如通过添加data事件、使用resume()方法或者pipe()方法等都可以将可读流从暂停模式切换为流动模式;使用paused()方法或者unpipe()方法可以将可读流从流动模式切换为暂停模式。

2.1.2、可读流的状态

在实际使用可读流时,它一共有3种状态,即初始状态(null)、流动状态(true)和非流动状态(false)。

当流处于初始状态(null)时,由于没有数据使用者,所以流不会产生数据,这时如果监听data事件、调用pipe()方法或resume()方法,都会将当前状态切换为流动状态(true),这样可读流即可开始主动地产生数据并触发事件。

如果调用pause()方法或者unpipe()方法,就会将可读流的状态切换为非流动状态(false),这将暂停流,但不会暂停数据生成。此时,如果再为data事件设置监听器,就不会再将状态切换为流动状态(true)了。

2.2、可读流的创建

Node.js中的可读流使用stream模块中的Readable类表示,因此可以直接使用下面代码创建:

js 复制代码
const readable = new stream.Readable();

另外,还可以使用fs模块的createReadStream方法创建可读流,语法格式如下:

js 复制代码
fs.createReadStream(path[, options])
  • path:读取文件的文件路径,可以是字符串、缓冲区或网址。
  • options:读取文件时的可选参数,可选值如下。
    • flags:指定文件系统的权限,默认为r。
    • encoding:编码方式,默认为null。
    • fd:文件描述符,默认为null。
    • mode:设置文件模式,默认为0o666。
    • autoClose:文件出错或者结束时,是否自动关闭文件,默认为true。
    • emitClose:流销毁时,是否发送close事件,默认为true。
    • start:指定开始读取的位置。
    • end:指定结束读取的位置。
    • highWaterMark:可读取的阈值,一般设置在16~100KB范围内。
js 复制代码
const fs = require("fs");
const read = fs.createReadStream("春夜喜雨.txt");

2.3、可读流的属性 方法及事件

可读流提供了很多属性、方法和事件,用来获取可读流信息、对可读流进行操作,以及监听可读流的相应操作,如下表:

常用属性:

属性 说明
destroyed 可读流是否已销毁,如果已经调用了readable.destroyO方法,则该属性值为true
readable 可读流是否被破坏、结束或者报错
readableEncoding 获取可读流的encoding属性
readableEnded 可读流是否已经没有数据,如果触发了end事件,则该属性值为rue
readableFlowing 可读流的当前状态
readableHighWaterMark 构造可读流时传入的high WaterMark的值
readableLength 获取准备读取的队列中的字节数(或对象数)

常用方法:

方法 说明
read([sizel) 从流中读取数据
setEncoding(encoding) 设置从流中读取的数据所使用的编码
pause() 暂停从可读流对象发出的data事件
ispaused() 获取可读流的当前操作状态
destroy() 销毁可读流
resume() 恢复从可读流对象发出的data事件
pipe(destination[,options]) 把可读流的输出传输到一个由deatination参数指定的Writable流对象
unpipe([destinationl) 分离附加的Writale流对象
filter(fn[,options]) 筛选流
forEach(fn[,options]) 迭代遍历流

常用事件:

事件 说明
close 当流或其数据源被关闭时,触发该事件
data 在流将数据块传送给使用者后触发
end 当流中没有数据可供使用时触发
error 当流由于底层内部故障而无法生成数据或尝试推送无效数据块时触发
pause 当调用stream.pause().且readsFlowing不为false时触发
readable 当有数据可从流中读取时触发
resume 当调用stream.resume().且readsFlowing不为true时触发

触发流事件同样采用on()方法实现,例如,下面代码创建了一个可读流,然后触发其data事件:

js 复制代码
const fs = require("fs")
let read = fs.createReadStream('春夜喜雨.txt')
read.on('data', (chunk)=>{
    console.log("读取到的数据:" + chunk.toString());
})
js 复制代码
读取到的数据:  春夜喜雨
          杜甫
好雨知时节,当春乃发生。
随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
晓看红湿处,花重锦官城。

2.4、可读流的常见操作

2.4.1、读取数据

使用read()方法可以从流中读取数据,其语法格式如下:

js 复制代码
对象名.read([size])
  • size:要读取的数据的字节数。
  • 返回值:返回值可能为字符串、Buffer、null等。
js 复制代码
const fs = require("fs");
const read = fs.createReadStream("春夜喜雨.txt", {encoding: 'utf-8'});
read.on('readable', ()=>{
     while (null !== (chunk = read.read(25))) {
          console.log(chunk.toString());
     }
});
2.4.2、设置编码格式

可读流读取数据时,默认情况下没有设置字符编码,流数据返回的是Buffer对象。如果设置了字符编码,则流数据返回指定编码的字符串。设置可读流中数据的编码格式需要使用setEncoding()方法,其语法格式如下:

js 复制代码
对象名.setEncoding(encoding)
js 复制代码
const fs = require("fs");
const read = fs.createReadStream("春夜喜雨.txt");
read.setEncoding("utf8")                       //设置编码格式
read.on('readable', function () {
     while (null !== (chunk = read.read(25))) {
        //设置编码格式后,此处不再需要使用toString()方法
        console.log(chunk);
     }                
});
复制代码
        春夜喜雨
          杜甫
好雨知时节,当春乃发生。

随风潜入夜,润物细无声。
野径云俱黑,江船火独明。
2.4.3、 暂停与恢复流

pause()方法可以使流动模式的可读流停止触发data事件,并切换为非流动模式,其语法格式如下:

js 复制代码
对象名.pause()

resume()方法可以恢复从可读流对象发出的data事件,将可读流切换为流动模式,其语法格式如下:

js 复制代码
对象名.resume()

按行输出故事内容:

js 复制代码
const fs = require('fs');
const read= fs.createReadStream("春夜喜雨.txt", {highWaterMark: 25});
read.setEncoding("utf8")                       //设置编码格式
read.on('data', (chunk)=>{
     console.log(chunk.toString());
     read.pause();
     setTimeout(()=>{
          read.resume();
     }, 1000);
});
read.on("close",()=>{
     console.log("读取完毕");
})
2.4.4、 获取流的运行状态

对可读流进行操作时,可以使用ispaused()方法判断流当前的操作状态,该方法不需要参数,返回结果为true或false,其语法格式如下:

js 复制代码
对象名.ispaused()
js 复制代码
const readable = new stream.Readable();
console.log(readable.ispaused());
readable.pause()
console.log(readable.ispaused());
复制代码
false
true

ispaused()方法主要用于readable.pipe()底层的机制,大多数情况下不会直接使用该方法。

2.4.5、 销毁流

使用destroy()方法可以销毁可读流,其语法格式如下:

js 复制代码
对象名.destroy([error])

该方法中有一个可选参数error,用于在处理错误事件时发出错误。

js 复制代码
const fs = require("fs")
const read = fs.createReadStream('春夜喜雨.txt')
read.setEncoding("utf8")
read.on('data', function (chunk) {
     console.log("读取到的数据:\n" + chunk.toString());
})
read.destroy()

上面代码运行结果为空,因为最后一行代码销毁了可读流read,可读流在销毁后,会将读取的数据清空,因此,通常在程序可能出现异常时,才会在处理异常的过程中使用销毁流方法。

2.4.6、 绑定可写流至可读流

readable.pipe()方法可以将可写流绑定到可读流,并将可读流自动切换到流动模式,同时将可读流的所有数据推送到绑定的可写流。pipe()方法的语法格式如下:

js 复制代码
可读流对象名.pipe(destination[, options])
  • destination:要绑定到可读流的可写流对象。
  • options:保存管道选项,通常为end参数,其参数值为true,表示如果可读流触发end事件,可写流也调用steam.end()结束写入,如果设置end值为false,则目标流就会保持打开。
  • 返回值:返回目标可写流。

pipe的含义为管道,比如要从A桶中向B桶中倒水,如果直接用A桶来倒水,那么水流可能会忽大忽小,而B桶中的水有可能因为溢出或者来不及使用而浪费,那么如何让水不浪费呢?这时就可以用一根水管连接A桶与B桶,这样A桶中的水通过水管匀速地流向B桶,B桶中的水就可以及时使用而不会造成浪费。流中的pipe也是如此,它是连接可读流和可写流的一条管道,可以实现读取数据和写入数据的一致性。这里需要说明的是,pipe()是可读流的方法,只能将可写流绑定到可读流,反之则不可以。

通过将可写流绑定至可读流为文件追加内容。

js 复制代码
const fs = require("fs");
const read = fs.createReadStream('春夜喜雨.txt');                    //创建可读流
const write = fs.createWriteStream('aaa.txt', {flags: "a"});  //创建可写流
read.pipe(write);                                              //将可写流绑定到可读流
console.log("已完成")
2.4.7、 解绑可写流

上文讲解了使用可读流的pipe()方法可以将可写流绑定到可读流,还可以通过可读流的unpipe()方法将已经绑定的可写流进行解绑,其语法格式如下:

js 复制代码
可读流对象名.unpipe([destination])

该方法中有一个可选参数destination,表示要解绑的可写流,如果该参数省略,表示解绑所有的可写流。

js 复制代码
const fs = require("fs");
const read = fs.createReadStream('春夜喜雨.txt');                    //创建可读流
const write = fs.createWriteStream('bbb.txt', {flags: "a"});  //创建可写流
read.pipe(write);                                              //将可写流绑定到可读流
console.log("已绑定可写流")
read.unpipe(write)                                             //解绑
console.log("已解绑可写流")

上面代码先为可读流read绑定一个可写流write,这时春夜喜雨.txt文件中的内容都被追加到"bbb.txt"文件中,然后将write解绑,于是从"bbb.txt"文件中移除春夜喜雨.txt中的内容,最终"凉州词.txt"文件中的内容将不会发生变化。

3、可写流的使用

3.1、可写流的创建

Node.js中的可写流使用stream模块中的Writable类表示,使用该类时,需要重写其中的write()方法,因此要使用stream模块中的Writable类创建可写流,需要使用的代码类似下面代码:

js 复制代码
const stream = require('stream');
const writable = new stream.Writable({
     write: function (chunk, encoding, next) {
          console.log(chunk.toString());
          next();
     }
});

另外,还可以使用fs模块的createWriteStream()方法创建可写流,语法格式如下:

js 复制代码
fs.createWriteStream(path[, options])
  • path:写入文件的文件路径。
  • options:写入文件时的可选参数,可选值如下。
    • flags:指定文件系统的权限,默认为w,如果要修改文件内容,而不是替换,需要将该值设置为a。
    • encoding:编码方式,默认为null。
    • fd:文件描述符,默认为null。
    • mode:设置文件模式,默认为0o666。
    • autoClose:文件出错或者结束时,是否自动关闭文件,默认为true。
    • emitClose:流销毁时,是否发送close事件,默认为true。
    • start:指定开始写入的位置。
    • highWaterMark:可写入的阈值,一般设置在16~100KB范围内。
js 复制代码
var fs = require("fs");
var write = fs.createWriteStream('demo.txt');  //创建可写流

3.2、可写流的属性 方法及事件

可写流提供了很多属性、方法和事件,用来获取可写流信息、对可写流进行操作,以及监听可写流的相应操作,它们的说明分别如表所示。

常用属性:

属性 说明
destroyed 可写流是否已销毁,如果已经调用了writable.destroy(),则为true
writable 可写流是否被破坏、报错或结束
writableEnded 可写流是否已经没有数据,如果在调用writable.end()之后,该值为rue
writableCorked 获取完全uncork流需要调用writable.uncork()的次数
writableFinished 可写流中的数据是否已传输完,在触发finish事件之前需将其设置为true
writableHigh WaterMark 返回构造可写流时传入的high WaterMark的值
writableLength 包含准备写入的队列中的字节数(或对象)
writableNeedDrain 如果流的缓冲区已满且流将发出drain,则为true

常用方法:

方法 说明
write() 写入数据
end() 通知可写流对象写入结束
setDefaultEncoding 为可写流设置默认的编码方式
end() 关闭可写流
destroy() 销毁可写流
cork() 强制把所有写入的数据都缓冲到内存中
uncork() 将调用stream.cork0方法缓冲的所有数据输出到目标

常用事件:

事件 说明
close 当可写流或数据源被关闭时,触发该事件
open 创建可写流的同时会打开文件,而打开文件就会触发该事件
drain 当写入缓冲区为空时触发该事件
error 写入或管道数据发生错误时触发该事件
finish 调用stream.end()且缓冲区数据都已传给底层系统之后触发该事件
pipe 当在可读流上调用stream.pipe()方法时会触发该事件,并将此可写流添加到其目标集
unpipe 在可读流上调用stream.unpipeO方法时会触发该事件,从其目标集中移除此可写流

3.3、可写流的常见操作

3.3.1、写入数据

使用write()方法可以向流中写入数据,其语法格式如下:

js 复制代码
对象名.write( chunk[, encoding, callback])
  • chunk:要写入的数据,其值可以是字符串、缓冲区或数组等。
  • encoding:可选参数,表示写入数据时的编码方式。
  • callback:可选参数,是一个回调函数,写入数据完成后执行。
js 复制代码
const fs = require("fs")
let txt = "这首诗抓住了边塞风光景物的一些特点,借其严寒春迟及胡笳声声来写战士们的心理活动,反映了边关将士的生活状"
    + "况。诗风苍凉悲壮,但并不低沉,以侠骨柔情为壮士之声,这仍然是盛唐气象的回响。"
//在文件原有内容后面追加内容,所以定义文件权限为"a"
let decr = fs.createWriteStream("凉州词.txt", {flags: "a"})
decr.write("\n鉴赏:\n" + txt, "utf8")                     //写入内容
3.3.2、设置编码方式

使用setDefaultEncoding()方法可以设置可写流的默认编码方式,其语法格式如下:

js 复制代码
对象名.setDefaultEncoding(encoding)
  • 参数encoding表示要设置的编码方式。
js 复制代码
const fs = require("fs")
const writeSteam = fs.createWriteStream("demo.txt")
writeSteam.setDefaultEncoding("utf8")              //设置编码方式
writeSteam.write("测试数据")                       //写入内容
3.3.3、关闭流

写入流的end()方法用来标识已经没有需要写入流中的数据了,因此通常用来关闭流,其语法格式如下:

js 复制代码
对象名.end([chunk[, encoding]][, callback])
  • chunk:可选参数,表示关闭流之前要写入的数据。
  • encoding:如果chunk为字符串,那么encoding为编码方式。
  • callback:流结束或者报错时的回调函数。
js 复制代码
const fs = require("fs")
const writeSteam = fs.createWriteStream("demo.txt")
writeSteam.setDefaultEncoding("utf8")              //设置编码方式
writeSteam.write("测试数据")                       //写入内容
writeSteam.end("写入完成")                         //关闭流
//writeSteam.write('继续写入');
复制代码
测试数据
写入完成

使用end()方法关闭流后,无法再向流中写入数据,否则将会产生异常,例如,去掉上面代码中最后一行的注释,再次运行时,将会出现错误提示。

3.3.4、销毁流

使用destroy()方法可以销毁所创建的写入流,并且流被销毁后,无法再向流写入数据。其语法格式如下:

js 复制代码
对象名.destroy([error])
  • 参数error为可选参数,表示使用error事件触发的错误。
js 复制代码
const fs = require("fs")
const writeSteam = fs.createWriteStream("demo.txt")
writeSteam.setDefaultEncoding("utf8")              //设置编码方式
writeSteam.write("测试数据")                       //写入内容
writeSteam.destroy()                               //销毁流

上面代码运行后,将会导致demo.txt文件中没有任何数据,因为虽然第4行代码中使用write()方法写入了数据,但由于紧接着销毁了写入流,这将导致使用该流执行的任何操作都会失效。因此,在使用写入流销毁操作时,通常在异常处理中使用该操作。

3.3.5、将数据缓冲到内存

使用写入流的cork()方法可以强制把所有写入的数据都缓冲到内存中,它的主要目的是为了适应将几个数据快速连续地写入流的情况。cork()方法不会立即将它们转发到底层目标处,而是缓冲所有数据块,直到调用uncork()方法。cork()方法的语法格式如下:

js 复制代码
对象名.cork()

当使用uncork()方法或end()方法时,缓冲区数据将被刷新。

js 复制代码
const stream = require('stream');
const writable = new stream.Writable({
     write: function (chunk, encoding, next) {
          console.log(chunk.toString());
          next();
     }
});
writable.write('天气晴朗');
writable.cork();
writable.write('阳光明媚');
复制代码
天气晴朗

通过观察上面结果,发现调用cork()方法后,接下来要输出的内容并没有显示。

3.3.6、输出缓冲后的数据

前面介绍了cork()方法,用以强制把所有写入的数据都缓冲到内存中,而使用uncork()方法可以将调用cork()方法后缓冲的所有数据输出到目标处。uncork()方法的语法格式如下:

js 复制代码
对象名.uncork()
js 复制代码
const stream = require('stream');
const writable = new stream.Writable({
     write: function (chunk, encoding, next) {
          console.log(chunk.toString());
          next();
     }
});
writable.write('天气晴朗');
writable.cork();
writable.write('阳光明媚');
writable.uncork();
复制代码
天气晴朗
阳光明媚

4、双工流与转换流

4.1、双工流

双工流Duplex可以实现流的可读和可写功能,即同时实现Readable和Writable。实现双工流需要进行以下3步。

  1. 继承Duplex类。
  2. 实现_read()方法。
  3. 实现_write()方法。
js 复制代码
const Duplex = require('stream').Duplex;
const myDuplex = new Duplex({
     _read(size) {
          // ...
     },
     _write(chunk, encoding, callback) {
          // ...
     }
});

示例:双工流的使用

js 复制代码
const stream = require('stream');
const duplexStream = stream.Duplex();
duplexStream._read = function () {
     this.push('读取数据');
     this.push(null)
}
duplexStream._write = function (data, enc, next) {
     console.log(data.toString());
     next();
}
duplexStream.on('data', data => console.log("监听:" + data.toString()));
duplexStream.on('end', data => console.log('监听:' + '读取完成'));
duplexStream.write('写入数据');
duplexStream.end();
duplexStream.on('finish', data => console.log('监听:' + '写入完成'));
js 复制代码
写入数据
监听:读取数据
监听:写入完成
监听:读取完成

4.2、转换流

转换流Transform其实也是双工流,它与Duplex的区别在于,Duplex虽然同时具备可读流和可写流的功能,但两者是相对独立的,而Transform中可读流的数据会经过一定的处理过程自动进入可写流。需要说明的是,从可读流到可写流,它们的数据量不一定相同。例如,常见的压缩、解压缩用的zlib就使用了转换流,压缩和解压缩前后的数据量明显不同。

实现转换流需要进行以下两步:

  1. 继承Transform类。
  2. 实现_transform()方法。_transform()方法用来接收数据,并产生输出(需要调用this.push(data),如果不调用,则接收数据但不输出)。当数据处理完后,必须执行callback(err, data)回调函数,该函数中的第一个参数用于传递错误信息,第二个参数用来输出数据(效果和this.push(data)相同),但参数可以省略。
js 复制代码
const Stream = require('stream')
class TransformReverse extends stream.Transform {
     constructor() {                               //继承构造函数
          super()
     }
     _transform(data, encoding, callback) {
          this.push(data);
          callback();
     }
}

示例:转换流的使用

js 复制代码
const Stream = require('stream');
class TransformStream extends Stream.Transform {
     constructor() {
          super()
     }
     _transform(data, encoding, callback) {
          //将写入的数据进行反转
          const res = data.toString().split('').reverse().join('');
          this.push(res)                                 //输出反转后的数据
          callback()
    }
}
var transformStream = new TransformStream();
transformStream.on('data', data => console.log(data.toString()))
transformStream.on('end', data => console.log('读取完成'));
transformStream.write('写入数据');
transformStream.end()
transformStream.on('finish', data => console.log('写入完成'));
复制代码
据数入写
读取完成
写入完成
相关推荐
rhythmcc2 小时前
【npm&pnpm】基本使用
前端·npm·node.js
吴佳浩 Alben2 小时前
Claude Code 源码泄露事件深度剖析
人工智能·arcgis·语言模型·自然语言处理·npm·node.js
如雨随行20202 小时前
【Vim】学习笔记(9)命令模式
笔记·学习·vim
如雨随行20202 小时前
【Vim】学习笔记(8)tips-2
笔记·学习·vim
流星雨在线2 小时前
Node.js + Express 项目完整搭建手册(Redis + MySQL + 常用中间件)
redis·node.js·express
流星雨在线2 小时前
SpringBoot 从开发到打包发布完整教程(对比 Node.js)
spring boot·后端·node.js
oh LAN3 小时前
主流 AI 编码工具对比表(2026 最新)
人工智能·编辑器·工具·代码
半个俗人3 小时前
07.Linux vi编辑器
linux·运维·编辑器
:mnong11 小时前
ClawX 项目设计分析
node.js·skill