Node之文件流
由于当我们在读取或者写入一些东西的时候,受限于内存和其他介质的数据大小和处理速度的限制,我们必须控制数据的速度,让它缓慢地流入从一个地方流入到另一个地方。比如:从源头流向内存,叫做可读流,从内存流向源头,叫可写流,还有一种双工流,可以双向流入。
文件流就是在磁盘和内存之间的数据流动。
可读流
const fs = require('fs');
const path = require('path');
const filePath = path.resolve(__dirname, './myfiles/a/1.txt');
const rs = fs.createReadStream(filePath, {
encoding: 'utf-8',
//start:1,
//end:1,
highWaterMark: 1,
autoClose: true//默认为true
})
rs.on('open',()=>{
console.log('文件被打开了')
})
rs.on('error',()=>{
console.log('文件发生错误')
})
rs.on('close',()=>{
console.log('文件被关闭了')
})
// rs.close()
let str=''
rs.on('data',(chunk)=>{
str+=chunk;
console.log('读到数据:',chunk)
rs.pause();
})
rs.on('pause',()=>{
console.log('暂停了')
setTimeout(() => {
rs.resume();
}, 1000);
})
rs.on('resume',()=>{
console.log('恢复了')
})
rs.on('end',()=>{
console.log('读取完毕',str)
})
文件流可以通过 fs.createReadStream 创建可读流,这个流是基于 stream 的 readable ,writable 两个大类中的可读类的子类。类似于事件,传递路径和配置方法,配置方法有编码格式,开始位置,结束位置,每次读取数量,这个数量取决于 encoding ,如果有值表示字符数,否则表示字节数,默认数量为 16kb 。
创建之后返回一个子类:readStream。子类中有一些事件。open 打开时触发,error 错误时触发,close 关闭时触发,可以设置 autoClose 自动关闭,默认为 true ,rs.close() 手动关闭,data 读取文件流,每次在读取前面设置的最大读取数后触发,然后接着读取下一个,读取到的数据格式也和前面设置有关,end 读取结束时触发。
还有两个方法,一个是暂停,暂停时触发 plause 事件,一个是恢复,恢复时触发 resume 事件。
可写流
const ws = fs.createWriteStream(filePath, {
encoding: 'utf8',
flags: 'w',
highWaterMark: 16 * 1024,
})
let i = 0;
function write() {
let flag = true;
while (i < 1024 * 1024 * 10 && flag) {
flag = ws.write('a');
i++;
}
}
write()
ws.on('drain', () => {
write();
})
ws.end('写入完毕');
类似于可读流,通过 fs.createWriteStream 来创建一个可写流。这个流会返回一个 writable 里面的子类 writeStream 。同样可以传入两个参数,第一个是路径,第二个是配置。配置中多了一个 flags ,这个在可读流中也可以传入,表示覆盖 w 或者追加 a 。highWaterMark 表示每次写入的最多字节数,这个配置与编码方式无关。如果编码方式为 utf-8 ,则会按照字符的格式写入到文件中。
ws 的事件除了 open ,error ,close 之外,还有一个 ws.write(data) 可以写入字符串或 buffer 。为了避免一次写入太多导致内存背压太高,在每次写入之后会返回一个 flag 。如果返回 true ,表示没有进入内存的写入队列,还可以在通道内写入,不用排队,如果为 false ,则表示写入通道已被排满,需要进入写入队列,这时无法写入。
但像上面的例子,当 flag 为false 之后,写入就停止了,所以需要 drain 事件来重新写入。drain 表示如果通道已经被清空,就会触发 drain 事件。然后就可以一直写入文件而不会导致背压卡顿的问题了。
ws.end([data]) 表示写入结束,关闭文件。当配置了 autoClose 时会自动关闭文件,默认为 true 。data 表示最后的一次写入数据。
async function method1() {
const from = path.resolve(__dirname, './myfiles/a/1.txt');
const to = path.resolve(__dirname, './myfiles/a/2.txt');
console.time('method1')
const content = await fs.promises.readFile(from);
await fs.promises.writeFile(to, content);
console.timeEnd('method1')
console.log('复制完毕');
}
// method1()
async function method2() {
const from = path.resolve(__dirname, './myfiles/a/1.txt');
const to = path.resolve(__dirname, './myfiles/a/2.txt');
console.time('method2')
const rs = fs.createReadStream(from);
const ws = fs.createWriteStream(to);
rs.on('data', chunk => {
const flag = ws.write(chunk)
if (!flag) {
rs.pause()
}
})
ws.on('drain', () => {
rs.resume()
})
rs.on('close', () => {
ws.end();
console.timeEnd('method2')
console.log('复制完成')
})
}
method2()
这是两种方法的对比。第一种会占用非常高的内存,所有的数据都会被读取到内存中,然后全部写入到磁盘中,会导致严重的背压问题,并且时间会增加。第二种采用流的方式来进行。每次写入时通道被占满就会停止读取,直到通道清空会恢复读取。这样就大大减少了内存的占用,并且时间也会减少很多。每次只会占用一个 chunk 的空间即 64kb ,非常节省内存。
决定文件效率的是执行效率,而不是运行效率。所以代码的增多反而会使得效率提高,就像一个死循环的代码会导致效率变低。
还可以使用 rs.pipe(ws) 这种方式来进行简化,就像在内存和磁盘搭起了一个管道,从磁盘到内存,再从内存到磁盘。并且会进行边读边写,写入占满会暂停读取,读取文件结束会写入关闭。
pipe 将可读流连接到可写流,返回参数,解决了大文件读取导致内存占用过高的背压问题。