fs读写操作和文件可读流的实现

246 阅读14分钟

前面已经提到过,我们可以使用 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断点调试章节

  1. new ReadStream 会创建一个可读流
  2. 可读流需要继承自 Readable 和 EventEmitter 类,文件的可读流内部实现实际使用的是 fs
  3. 创建可读流,默认调用了 ReadStream 自己的 open 事件,open 事件内部就是调用 fs.open() 方法
  4. 打开文件后,直接开始读取,读取的时候会调用父类的 Readable 上的 read 方法,该方法内部调用的 this._read(),_read 又是在子类 ReadStream 上实现的,父类没有具体的实现,都是子类自由扩展 _read 方法,父类被称为抽象类。
  5. 子类调用父类的 push 方法,父类会自动触发 emit("data"),上一步的好处就来了,父类不关心子类使用何种方法派发何种数据,这里只要你调 push,我就帮你传输数据,这也是文件流能使用 fs 读写文件能力的根本原因。
  6. 读取完毕后 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 源码内部实现

和文件可写流的差别:

  1. 可读流子类继承 stream.Readable, 可写流子类继承了 stream.Writable
  2. 可读流调用的是read -> _read(fs.read), 可写流 write -> _write(fs.write)

可以看到,可读流和可写流的实现套路基本也一致,这里就不过多赘述了。