前面已经提到过,我们可以使用 fs 来操作文件,但之前我们使用的大多是同步的方式,究竟什么时候用同步,什么时候用异步呢?
同步和异步的场景
假如在我们服务启动之前,我们可以同步读取一些配置文件,如果程序运行起来,就不要采用同步读写了,因为这样会造成线程阻塞,具体还是要分业务场景,基本准则就是:
- 如果程序允许阻塞在这里,可以采用同步
- 否则,必须使用异步。
fs 异步实现文件拷贝
核心思想就是先把读取的内容放到内存中,然后把内存中的内容写入到文件中。
// @1 writeFile 如果写入的文件不存在,会创建文件,如果文件存在,会清空原文件内容
// @2 writeFile 如果写入文件的文件夹不存在,会报错哦
const fs = require('fs');
const path = require('path');
fs.readFile(path.resolve(__dirname, './1.txt'), 'utf8', function(err, data) {
fs.writeFile(path.resolve(__dirname, './2.txt'), data, function(err) {
console.log('写入成功');
});
});
为什么需要 stream(流)
上述代码的性能是特别差的,如果这个文件特别大,比如 100G 的文件,这个操作就是特别危险的,内存直接就爆了,所以这种方式就不适合大文件读取。
针对大文件的读写,我们一般使用流,流的特点就是有方向,可以分段读写,防止淹没可用内存,这个需要依赖底层的文件操作,需要对文件进行精确的读取(读某一部分),这需要一些列的 api:
// @1 fs.open 先把要读的文件打开,获取文件操作描述符
// @2 fs.read 读取这个文件操作描述符,获取部分文件内容
// @3 fs.open 再把要写的文件打开,获取文件操作描述符
// @4 fs.write 通过文件操作描述符,写入对应文件读取到的内容
// ...
// @5 读取完毕关闭文件操作描述符
当然这些 api 我们平时开发用不到,提到这个只是为了了解 stream 底层的实现原理。
比如我们读取 9 个数字,正常我们需要 9 个字节的内存空间先把全部数字读取到,而使用流,我们可以做到三个三个的读取,从占用 9 字节的内存锐减到 3 字节,这就是我们提到的流在读取大文件时可以防止淹没可用内存的原因。
fs 实现分段读写(伪代码 类似 http 206 范围读取)
现在我们 1.txt 中有 0 - 9 十个数字,我们想实现 3 个 3 个的读写。
code
// 敲黑板
// @1 open 方法可以传入一个 flag,标识操作文件的方式,常用的几种如下:
// + r 读取 文件不存在就报错
// + w 写入 文件不存在会创建
// + a 追加
// + r+ 能读能写,以读取的行为为准,文件不存在会报错
// + w+ 能写能读,以写入的行为为准,文件不存在会创建
// @2 open 第三个参数是操作文件的权限, 1 执行 4 读取 2写入,默认是 0o666 的进制组合,
// 代表我,我的群组,别人都可读可写
// @3 fd 文件描述符,是个数字类型,用完需要关闭掉
const fs = require('fs');
const path = require('path');
const buf = Buffer.alloc(3);
fs.open(path.resolve(__dirname, '1.txt'), 'r', 0o666, function(err, fd) {
fs.open(path.resolve(__dirname, '2.txt'), 'w', function(err, wfd) {
function close() {
fd.close(fd, () => {});
wfd.close(wfd, () => {});
}
// 读 + 写
function next() {
// 从 buf 0 开始写,写入三个字节,从文件 0 开始读取
fs.read(fd, buf, 0, 3, 0, function(err, bytesRead) {
startIdx += 3; // 开始读取的下标
// bytesRead 实际读取到的个数
if (bytesRead == 0) {
return close(); // 关闭文件描述符
}
fs.write(wfd, buf, 0, 3, 0, function(err, bytesWitten) {
console.log(bytesWitten);
next();
});
});
}
next(); // 读 + 写 递归
});
});
基于文件系统操作的流(文件流)
上面那样读写逻辑完全耦合在一起是比较痛苦的,我们考虑把读和写完全拆分成两块,但是写入又依赖读怎么办呢?这时候,我们应该想到发布订阅,你读完三个通知我一下,我立马写入三个,node 中的文件流可以做这个事情,注意,这个流是基于 fs 的,不是我们后面要说的 stream 模块。
文件流之可读流(fs.createReadStream)
用法示例
// 文件流是文件操作中自己实现的流 文件流是基于 stream 的,底层的实现用的是 fs.read, fs.open...
const fs = require('fs');
const path = require('path');
const rs = fs.createReadStream(path.resolve(__dirname, '1.txt'), {
flags: 'r', // r 读取,供 fs.open 使用
encoding: null, // 默认读取出来的是 buffer 类型
autoClose: true, // 读取完毕后需要关闭流, fs.close
emitClose: true, // 读取完毕后要触发 close 事件, emit('close')
start: 0,
end: 4, // start end 表示就是从索引 0 - 4 的结果, 也就是一共读 5 个字节
highWaterMark: 2 // 每次读 2 个,也就是读三次
});
// open 和 close 是文件流特有的
rs.on('open', function(fd) {
console.log('1.txt is opened');
});
const arr = []; // buffer 片段列表
// 绑定后不停触发,讲内部数据传递出来
rs.on('data', function(data) {
console.log('buffer 片段', data);
rs.pause(); // 控制暂停读取
arr.push(data);
});
// 数据读取完毕的回调
rs.on('end', function(data) {
console.log(Buffer.concat(arr).toString());
});
// 关闭文件(描述符)
rs.on('close', function() {
console.log('close');
});
// 错误回调
rs.on('error', function(err) {
console.log(err);
});
// 控制 1s 读取一次
setInterval(() => {
rs.resume(); // 恢复触发 data 事件
}, 1000);
手动实现可读流(发布订阅)
当然这个源码不是完整版的,缺少 pipe 方法,往下看哦,提到 pipe 时候会继续扩展。
code
// ysReadStream.js
const EventEmitter = require('events');
const fs = require('fs');
// 继承 events 发布订阅的能力
class ReadStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || 'r'; // r 读取,供 fs.open 使用
this.encoding = options.encoding || null; // 默认读取出来的是 buffer 类型
this.autoClose = options.autoClose || true, // 读取完毕后需要关闭流, fs.close
this.emitClose = options.emitClose || true, // 读取完毕后要触发 close 事件, emit('close')
this.start = options.start || 0,
this.end = options.end || undefined, // start end 决定读取多少个字节
this.highWaterMark = options.highWaterMark || 1024 * 64 // 每次读多少 默认一次 64kb
this.flowing = false; // 默认暂停流动(不读取文件内容),后续 pause resume 就是更新该属性
this.offset = this.start; // 读取文件的偏移量
this.open();
// events 内置 newListener 回调,要在绑定其他事件之前绑定
// 每次调用 on 方法,如果不是 newListener 事件,就会同步触发 newListener 回调
this.on('newListener', type => {
if (type == 'data') {
this.flowing = true;
this.read(); // 绑定 data 事件就开始读取
}
});
}
destroy(err) {
if (err) {
this.emit('error', err);
}
if (this.autoClose) {
fs.close(this.fd, () => {
if (this.emitClose) {
this.emit('close');
}
});
}
}
read() {
// 注册 data 时拿不到 this.fd,因为读是先于 open 的,所以在 open 事件回调中调用再次 read 方法
if (typeof this.fd !== 'number') {
// 只触发一次的 once 方法
return this.once('open', () => this.read());
}
// 根据用户提供的 start 和 end 来进行读取 类似 http 206 header 头里的 range 作用
let howMuchToRead = this.end ? Math.min(this.highWaterMark, this.end - this.offset + 1) : this.highWaterMark;
const buffer = Buffer.alloc(howMuchToRead); // 声明 buff
fs.read(this.fd, buffer, 0, howMuchToRead, this.offset, (err, bytesRead) => {
if (bytesRead) {
this.offset += bytesRead; // 增加读取文件的偏移量
this.emit('data', buffer.slice(0, bytesRead)); // 触发 data 事件
// 根据 flowing 决定是否继续读
if (this.flowing) {
this.read(); // 继续读
}
} else {
this.emit('end');
this.destroy();
}
});
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
return this.destroy(err);
}
this.fd = fd;
this.emit('open', fd);
})
}
// 恢复读取数据
resume() {
if (!this.flowing) {
this.flowing = true;
this.read();
}
}
// 暂停读取数据
pause() {
this.flowing = false;
}
}
module.exports = ReadStream;
测试代码
// 文件流是文件操作中自己实现的流 文件流是基于 stream 的,底层的实现用的是 fs.read, fs.open...
const path = require('path');
const ReadStream = require('./ysReadStream');
const rs = new ReadStream(path.resolve(__dirname, '1.txt'), {
flags: 'r', // r 读取,供 fs.open 使用
encoding: null, // 默认读取出来的是 buffer 类型
autoClose: true, // 读取完毕后需要关闭流, fs.close
emitClose: true, // 读取完毕后要触发 close 事件, emit('close')
start: 0,
end: 4, // start end 表示就是从索引 0 - 4 的结果, 也就是一共读 5 个字节
highWaterMark: 2 // 每次读 2 个,也就是读三次
});
// open 和 close 是文件流特有的
rs.on('open', function(fd) {
console.log('1.txt is opened');
});
const arr = []; // buffer 片段列表
// 绑定后不停触发,讲内部数据传递出来
rs.on('data', function(data) {
console.log('buffer 片段', data);
rs.pause(); // 控制暂停读取
arr.push(data);
});
// 数据读取完毕的回调
rs.on('end', function(data) {
console.log(Buffer.concat(arr).toString());
});
// 关闭文件(描述符)
rs.on('close', function() {
console.log('close');
});
// 错误
rs.on('error', function(err) {
console.log(err);
});
// 控制 1s 读取一次
setInterval(() => {
rs.resume(); // 恢复触发 data 事件
}, 1000);
分析 readStream 源码内部实现
继续断点调试,集体方法不再赘述,可以参考 实现commonJS 规范 - Node断点调试章节
- new ReadStream 会创建一个可读流
- 可读流需要继承自 Readable 和 EventEmitter 类,文件的可读流内部实现实际使用的是 fs
- 创建可读流,默认调用了 ReadStream 自己的 open 事件,open 事件内部就是调用 fs.open() 方法
- 打开文件后,直接开始读取,读取的时候会调用父类的 Readable 上的 read 方法,该方法内部调用的 this._read(),_read 又是在子类 ReadStream 上实现的,父类没有具体的实现,都是子类自由扩展 _read 方法,父类被称为抽象类。
- 子类调用父类的 push 方法,父类会自动触发 emit("data"),上一步的好处就来了,父类不关心子类使用何种方法派发何种数据,这里只要你调 push,我就帮你传输数据,这也是文件流能使用 fs 读写文件能力的根本原因。
- 读取完毕后 push(null), 父类触发 emit("end")
代码模拟 ReadStream 的源码层面具体实现流程
const { Readable } = require('stream');
class MyStream extends Readable {
constructor() {
super();
this.open();
}
open() {
// 这里触发文件打开回调,注意要延迟触发,不然回调还没注册呢
process.nextTick(() => {
this.emit('start_open');
});
// open 方法会调用父类的 read 方法哦
this.read();
}
_read() {
// 父类的 read 方法 会默认调用子类的 _read 方法哦
console.log('子类的_read 被 父类的 read 调用,这里执行');
}
}
const myStream = new MyStream();
myStream.on('start_open', function() {
console.log('打开文件的回调,然后立马就要读文件啦');
});
更精简的实现
不过以上代码只是为了帮助我们理解 ReadStream 的具体实现流程,我们实际封装起来可以更简化,因为 data 方法注册的时候也会去调用 this.read 方法,所以就不用手动调用 open 了
const { Readable } = require('stream');
class MyStream extends Readable {
constructor() {
super();
this.index = 0;
}
_read() {
if (this.index++ == 5) {
return this.push(null);
}
// 通知父类触发 data 回调 这里只能是字符串或者 buffer
this.push('abc');
}
}
const myStream = new MyStream();
myStream.on('data', function(data) {
console.log('读到东西了', data);
});
myStream.on('end', function(data) {
console.log('读完了', data);
});
// 读到东西了 <Buffer 61 62 63>
// 读到东西了 <Buffer 61 62 63>
// 读到东西了 <Buffer 61 62 63>
// 读到东西了 <Buffer 61 62 63>
// 读到东西了 <Buffer 61 62 63>
// 读完了 undefined
我们可以看到, ReadStream 只是继承了 Readable 类,内部实现了 _read 方法而已。
文件流之可写流(fs.createWriteStream)
可写流中有三个重要的方法,分别是 write、end、on('drain')。
用法示例
// 可写流用法示例
const fs = require('fs');
const path = require('path');
const ws = fs.createWriteStream(path.resolve(__dirname, '1.txt'), {
flags: 'w', // w 写入,供 fs.open 使用
encoding: null, // 默认读取出来的是 buffer 类型
mode: 0o666, // 文件权限
autoClose: true, // 读取完毕后需要关闭流, fs.close
emitClose: true, // 读取完毕后要触发 close 事件, emit('close')
start: 0,
highWaterMark: 2 // 缓冲区大小,其中第一个会被直接写入,其余在缓冲区等待被写入(防止异步写同一个文件乱序)
});
// open 和 close 是文件流特有的
ws.on('open', function(fd) {
console.log('1.txt is opened');
});
// string | buffer
// 返回值 flag 代表写入内容是否没达到水位线(highWaterMark)
// 没达到预期就返回 true,达到预期返回 false
let flag = ws.write('1', 'utf8', function() {
console.log('写入成功1');
});
flag = ws.write('2', 'utf8', function() {
console.log('写入成功2');
});
console.log(flag);
ws.on('drain', function() {
// 达到水位线,当前且缓冲区队列的内容全部被写入到文件中,清空写入计数并触发此回调
// 注意,不是达到水位线立刻触发,还需要本轮缓冲区缓存的数据(根据水位线来定的)全部写入
// 比如 12345 水位线是 2
// 1 2 触发一次 drain,写入数 2 -> 0
// 2 3 触发一次 drain,写入数 2 -> 0
// 5 不触发 drain,写入数 1,当前缓冲区虽全部写入,但是没达到水位线
});
// 写入完成,可以追加一些内容,并且关闭文件
setTimeout(() => {
ws.end('死了');
}, 1000);
// false
// 1.txt is opened
// 写入成功1
// 写入成功2
手动实现可写流
code
// ysWriteStream.js
const EventEmitter = require('events');
const fs = require('fs')
class WriteStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || 'w'; // w 写入,供 fs.open 使用
this.encoding = options.encoding || null; // 默认读取出来的是 buffer 类型
this.autoClose = options.autoClose || true, // 读取完毕后需要关闭流, fs.close
this.emitClose = options.emitClose || true, // 读取完毕后要触发 close 事件, emit('close')
this.start = options.start || 0,
this.highWaterMark = options.highWaterMark || 1024 * 16 // 写的预期是 16k
this.open();
// 自定义写入流的属性
// 1. 记录当前批次待写入总个数,如果写入文件后,再减去写入的个数
this.len = 0; // 字节数
this.offset = this.start; // 写入时的偏移量
this.cache = []; // 缓存写入操作
this.needDrain = false; // 是否要触发 drain 事件 正在写的和当前缓冲区全部写入触发一次 drain
this.writing = false; // 标识是否正在写入,如果是正在写入就放到缓冲区
}
clearBuffer() { // 消息队列
// 写入完毕后 清空缓存区
let obj = this.cache.shift(); // 底层用链表来优化队列
if (obj) {
// 有数据递归写
console.log('写入中', obj);
this._write(obj.chunk, obj.encoding, obj.cb)
} else {
console.log('写入完成')
this.writing = false;
// 一轮写入完成 判断是否需要触发 drain
if (this.needDrain) {
this.needDrain = false;
this.emit('drain')
}
}
}
write(chunk, encoding = null, cb = () => {}) {
// 转 buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.len += chunk.length;
this.needDrain = this.len >= this.highWaterMark; // 是否需要触发 drain
let oldCb = cb;
// 强化 cb 方法
cb = () => {
oldCb();
this.clearBuffer()
}
if (this.writing) {
// 放入缓存区
this.cache.push({
chunk,
encoding,
cb
});
} else {
this.writing = true;
// 真正进行写入操作
this._write(chunk, encoding, cb);
}
return !this.needDrain;
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once('open', () => this._write(chunk, encoding, cb))
}
fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, written) => {
this.offset += written;
this.len -= written;
cb(); // 成功后会调用回调,回调完成后需要继续清空
})
}
end(chunk, encoding = null, cb = () => {}) {
}
destroy(err) {
if (err) {
this.emit('error', err);
}
if (this.autoClose) {
fs.close(this.fd, () => {
if (this.emitClose) {
this.emit('close');
}
});
}
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
return this.destroy(err);
}
this.fd = fd;
this.emit('open', fd);
})
}
}
module.exports = WriteStream
测试代码
const fs = require('fs');
const path = require('path');
const WriteStream = require('./ysWriteStream');
const ws = new WriteStream(path.resolve(__dirname, '1.txt'), {
flags: 'w', // w 写入,供 fs.open 使用
encoding: null, // 默认读取出来的是 buffer 类型
mode: 0o666, // 文件权限
autoClose: true, // 读取完毕后需要关闭流, fs.close
emitClose: true, // 读取完毕后要触发 close 事件, emit('close')
start: 0,
highWaterMark: 3 // 缓冲区大小,其中第一个会被直接写入,其余在缓冲区等待被写入(防止异步写同一个文件乱序)
});
let index = 0;
function write(){
let flag = true
while (index != 10 && flag) {
flag = ws.write(index++ + '')
};
}
write();
// 此方法 需要保证当写入的数据达到预期后,并且缓冲区数据全部被清空写入到文件中,才会触发
ws.on('drain',function () {
write()
console.log('drain')
})
模仿底层队列 + 链表优化时间复杂度
linkedList.js
// 线性结构 栈 (先进的后出) 队列 (先进入的先出) 链表 (数据结构的核心都是存储数据的)
// 用链表来实现栈、队列结构
// 链表就是关联每一个节点数据来使用,删除头部和尾部,向前追加 向后追加是有优势
// 单向(只有next)、双向(next、prev) 循环 tail.next-》 head head.next-》 tail
// 双向链表 (单向链表)
class Node {
constructor(element, next) {
this.element = element;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0; // 链表的总长度
}
_getNode(index) {
let head = this.head;
for (let i = 0; i < index; i++) {
head = head.next;
}
return head;
}
add(index, element) { // 可以传递索引,可以不传递索引
if (arguments.length == 1) {
element = index;
index = this.size;
}
if (index == 0) { // 把链表的头给改了
let oldHead = this.head; // 取出来老的头
this.head = new Node(element, oldHead)
} else {
let prevNode = this._getNode(index - 1); // 永远找的都是当前索引的前一个节点
prevNode.next = new Node(element, prevNode.next)
}
this.size++;
}
remove(index) {
let removeNode
if (index == 0 ) {
removeNode = this.head;
if(removeNode == null) return;
this.head = this.head.next;
} else {
let prevNode = this._getNode(index - 1);
if (!prevNode) return;
removeNode = prevNode.next
prevNode.next = prevNode.next.next
}
this.size--;
return removeNode
}
update(index, element) {
let node = this._getNode(index);
node.element = element;
return node;
}
getNode(index) {
return this._getNode(index);
}
reverse() {
function reverse(head) {
if (head == null || head.next === null) return head;
let newHead = reverse(head.next); // 新头变为下一个
head.next.next = head; // 让下一个人的next 指向老的头
head.next = null; // 老的头下一个指向是的null
return newHead; // newHead, 就是不停的向下找 最后一个,最后一个就是头
}
return reverse(this.head);
}
reverse1() {
let head = this.head;
if (head === null || head.next == null) return head;
let newHead = null;
while (head) {
let n = head.next; // 通过n 来引用链表,否则直接将head.next = null 后面的就回收了
head.next = newHead; // 把当前的下一个指向新的链表头
newHead = head; // 把第一项搬家到新的链表中
head = n;
}
return newHead
}
}
class Queue {
constructor() {
this.ll = new LinkedList
}
add(element) {
this.ll.add(element)
}
peak() {
return this.ll.remove(0);
}
}
// 链表的反转, 如何反转一个单向链表
exports.LinkedList = LinkedList;
exports.Queue = Queue;
修改 ysWriteStream.js
const EventEmitter = require('events');
const fs = require('fs')
const { Queue } = require('./linkedList');
class WriteStream extends EventEmitter {
constructor(path, options = {}) {
super();
this.path = path;
this.flags = options.flags || 'w'; // w 写入,供 fs.open 使用
this.encoding = options.encoding || null; // 默认读取出来的是 buffer 类型
this.autoClose = options.autoClose || true, // 读取完毕后需要关闭流, fs.close
this.emitClose = options.emitClose || true, // 读取完毕后要触发 close 事件, emit('close')
this.start = options.start || 0,
this.highWaterMark = options.highWaterMark || 1024 * 16 // 写的预期是 16k
this.open();
// 自定义写入流的属性
// 1. 记录当前批次待写入总个数,如果写入文件后,再减去写入的个数
this.len = 0; // 字节数
this.offset = this.start; // 写入时的偏移量
this.cache = new Queue; // 缓存写入操作
this.needDrain = false; // 是否要触发 drain 事件 正在写的和当前缓冲区全部写入触发一次 drain
this.writing = false; // 标识是否正在写入,如果是正在写入就放到缓冲区
}
clearBuffer() { // 消息队列
// 写入完毕后 清空缓存区
let obj = this.cache.peak(); // 底层用链表来优化队列
if (obj) {
// 有数据递归写
console.log('写入中', obj);
let { chunk, encoding, cb } = obj.element;
this._write(chunk, encoding, cb)
} else {
console.log('写入完成')
this.writing = false;
// 一轮写入完成 判断是否需要触发 drain
if (this.needDrain) {
this.needDrain = false;
this.emit('drain')
}
}
}
write(chunk, encoding = null, cb = () => {}) {
// 转 buffer
chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
this.len += chunk.length;
this.needDrain = this.len >= this.highWaterMark; // 是否需要触发 drain
let oldCb = cb;
// 强化 cb 方法
cb = () => {
oldCb();
this.clearBuffer()
}
if (this.writing) {
// 放入缓存区
this.cache.add({
chunk,
encoding,
cb
});
} else {
this.writing = true;
// 真正进行写入操作
this._write(chunk, encoding, cb);
}
return !this.needDrain;
}
_write(chunk, encoding, cb) {
if (typeof this.fd !== 'number') {
return this.once('open', () => this._write(chunk, encoding, cb))
}
fs.write(this.fd, chunk, 0, chunk.length, this.offset, (err, written) => {
this.offset += written;
this.len -= written;
cb(); // 成功后会调用回调,回调完成后需要继续清空
})
}
end(chunk, encoding = null, cb = () => {}) {
}
destroy(err) {
if (err) {
this.emit('error', err);
}
if (this.autoClose) {
fs.close(this.fd, () => {
if (this.emitClose) {
this.emit('close');
}
});
}
}
open() {
fs.open(this.path, this.flags, (err, fd) => {
if (err) {
return this.destroy(err);
}
this.fd = fd;
this.emit('open', fd);
})
}
}
module.exports = WriteStream;
可读流结合可写流 (pipe)
code
const ReadStream = require('./ysReadStream');
const WriteStream = require('./ysWriteStream');
const path = require('path')
let rs = new ReadStream(path.resolve(__dirname,'1.txt'),{
highWaterMark: 4
});
let ws = new WriteStream(path.resolve(__dirname,'2.txt'),{
highWaterMark: 1
})
rs.on('data', function(chunk) {
let flag = ws.write(chunk);
if (!flag) {
rs.pause();
}
});
ws.on('drain', function() {
rs.resume();
});
// 写入完成
// 写入完成
// 写入完成
这样写虽然也能满足我们的需求,但是用起来也这么费劲,也太长了,官方的可读流提供了 pipe 方法,如下:
官方 api 警告
const path = require('path')
const fs = require('fs')
const rs = fs.createReadStream(path.resolve(__dirname,'1.txt'));
const ws = fs.createWriteStream(path.resolve(__dirname,'2.txt'))
rs.pipe(ws);
ok,那我们来给 rs 添加 pipe 方法
扩展 ysReadStream.js,添加 pipe 方法
// ysReadStream.js
class ReadStream extends EventEmitter {
// ...
// 把 ws 传入 rs 中做 pipe 操作
pipe(ws) {
const rs = this;
rs.on('data', function(chunk) {
let flag = ws.write(chunk);
if (!flag) {
rs.pause();
}
});
ws.on('drain', function() {
rs.resume();
});
}
// ...
}
使用方式,这段代码故意不收起来的,因为这很 show time
const ReadStream = require('./ysReadStream');
const WriteStream = require('./ysWriteStream');
const path = require('path')
let rs = new ReadStream(path.resolve(__dirname,'1.txt'),{
highWaterMark: 4
});
let ws = new WriteStream(path.resolve(__dirname,'2.txt'),{
highWaterMark: 1
})
// 这个方法是异步的,会读一点写一点,可以支持大文件的操作
rs.pipe(ws);
分析 readStream 源码内部实现
和文件可写流的差别:
- 可读流子类继承 stream.Readable, 可写流子类继承了 stream.Writable
- 可读流调用的是read -> _read(fs.read), 可写流 write -> _write(fs.write)
可以看到,可读流和可写流的实现套路基本也一致,这里就不过多赘述了。