有了可读流,又有了可写流,两件快乐事重合在一起,为什么会变成这样呢……

258 阅读7分钟

什么是流?

根据Node.js官网解释,流(stream)是一种在 Node.js 中处理流式数据的抽象接口。
1、抽象接口就是将数据的传输隐喻为水管之间的连接,水管里面的水就是数据
2、是有方向、可连接的
3、数据格式通常是Buffer

  • 流共分四种

Writable(可写流)
Readable(可读流)
Duplex(可读写流,又称双工流)
Transform(属于Duplex的一种)

  • Node.js的诸多方法如http请求都是流的实例
  • 而流是EventEmitter的实例(容易被忽视)

可写流

一般来说,可写流中我们用write方法和监听drain事件居多
现在我们来实现可写流

let fs = require('fs');
let EventEmiter = require('events');
// 根据可写流的输出
// 当highWaterMark小于我们写的内容长度 先触发drain 后触发回调函数
// 当highWaterMark大于我们写的内容长度 先触发回调函数一次 再触发drain 再触发剩余回调函数
// 虽然很奇怪但是没办法 = =
// 所以我定了个fisrt变量确定highWaterMark和内容长度的关系
let first = undefined;
let firstFlag = true;
/** 
 * --------------------------------------------------
 * Node.js源代码中
 * WriteStream继承(call)Writable
 * Writable继承(call)Stream
 * Stream继承(call)EE(EventEmiter)
 * 是一个链式关系
 * 所以我们直接继承EventEmiter
 * 并且将Writable中的write等方法写在WriteStream中
 * 
 * @param {string}  path    地址
 * @param {object}  options 配置
 * @extends EventEmiter
 * @this  WriteStream
 * --------------------------------------------------
 */
class WriteStream extends EventEmiter {
  constructor(path, options) {
    super();
    // 设置options配置
    options = copyObject(getOptions(options, {}));
    // path必须是string类型
    if (typeof path !== 'string') throw Error('path must be string');
    this.path = path;
    // 从第几个开始写
    this.start = options.start || 0;
    // 文件标识符
    this.fd = options.fd === undefined ?null:options.fd;
    // 默认write
    this.flags = options.flags === undefined ? 'w' : options.flags;
    // 默认0o666
    this.mode = options.mode === undefined ? 0o666 : options.mode;
    // 默认自动关闭
    this.autoClose = options.autoClose === undefined ? true : options.autoClose;
    // 编码默认utf8
    this.encoding = options.encoding || 'utf8';
    // 高水位线 默认64k
    this.highWaterMark = options.highWaterMark || 16 * 1024;
    // 触发drain事件的条件
    this.needDrain = false;
    // 是否正在写入,初始为未写
    this.iswriting = false;
    // 缓存
    this.cache = [];
    // 缓存长度
    this.cacheLength = 0;
    // 偏移量,第一次后每次开始写的位置
    this.pos = this.start;
    // write回调函数 储存
    this.cbCache = [];
    // 一创建,就执行open
    this.open();
  }
  // 打开文件
  open() {
    /**
     * 打开文件
     * @param {string|Buffer|URL} path 地址
     * @param {string|number} flags 默认w(会先清空文件内容)
     * @param {integer} mode 读写权限
     * --------------------------------------------------
     */
    fs.open(this.path, this.flags, this.mode, (err, fd) => {
      if (err) {
        this.emit('error');
        this.destory();
        return;
      }
      this.fd = fd;
      this.emit('open');
    })
  }
  // 关闭文件
  destory() {
    // 存在文件标识符fd 则关闭
    if (typeof this.fd === 'number') {
      fs.close(this.fd, () => {
        // 触发close
        this.emit('close');
      });
      return;
    }
    // 触发close
    this.emit('close');
  }
  /**
   * write 外部调用的 write方法
   * @param {string|Buffer}    chunk     写入内容
   * @param {string}    encoding  编码
   * @param {function}  callback  回调函数
   * --------------------------------------------------
   */
  write(chunk, encoding = this.encoding, callback) {
    // 不是Buffer格式 就转成 Buffer格式
    if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
    // 缓存长度增加 增加数量为 chunk长度
    this.cacheLength += chunk.length;
    // needDrain代表是否需要触发drain
    // 如果缓存长度 大于 高水位线,代表数据量超过最高通过量,needDrain为true
    // 如果缓存长度 小于 高水位线,代表数据量低于最高通过量,needDrain为false
    this.needDrain = this.highWaterMark <= this.cacheLength;
    
    if(firstFlag){
      first = this.needDrain;
      firstFlag = false;
    };console.log(first);
    // 当正在写时,缓存增加,写完在写
    if (this.iswriting) {
      this.cache.push({
        chunk,
        encoding,
        callback
      })
    }else{
      // 实际第一次开始写的位置,使iswriting为true
      this.iswriting = true;
      // 内部write方法,传入chunk、encoding、
      this._write(chunk,encoding,()=>this.clearBuffer(),callback);
    }
    // write必须有为布尔值的返回值
    return !this.needDrain;
  }
  /**
   * 内部的_write方法
   * @param {string|Buffer} chunk 写入内容
   * @param {string} encoding 编码
   * @param {function} clear clearBuffer函数
   * @param {function} callback write的回调函数
   */
  _write(chunk,encoding,clear,callback){
    //如果write时 还没获取到fd
    if(typeof this.fd != 'number'){
      //就当触发open时 就会再次调用
      this.once('open',()=>this._write(chunk,encoding,clear,callback));
      return;
    }
    // 真正开始写
    fs.write(this.fd,chunk,0,chunk.length,this.pos,(err,bytesHasWrite)=>{
      // 偏移量增加 大小为写入长度
      this.pos += bytesHasWrite;
      // 缓存长度减小 大小为写入长度
      this.cacheLength -= bytesHasWrite;
      if(callback)this.cbCache.push(callback);
      //调用清除Buffer函数
      clear();
    })
  }
  clearBuffer(){
    // 获取缓存的第一个
    let buf = this.cache.shift();
    // 若存在第一个
    if(buf){
      // 再写
      // chunk、encodeing、callback全是buffer的属性
      this._write(buf.chunk,buf.encoding,()=>this.clearBuffer(),buf.callback)
    }else{
      // 啊!!终于写完了
      // 判断needDrain,当需要就触发
      // 如果第一次传入数据未到达水位线
      // console.log(this.needDrain);
      if(this.needDrain){
        // 写完了 不写了
        this.iswriting = false;
        // 触发过了,下次自来
        this.needDrain = false;
        // 第一个要在drain前触发
        // 因为无论如何都写了
        // 将cbCache第一项shift出来并执行
        if(first){
          // 先触发drain
          this.emit('drain');
          this.cbCache.shift()();
        }else{
          // 先触发一次回调
          this.cbCache.shift()();
          this.emit('drain');
        }
      }
      // 执行所有callback
      // this.cbCache.shift();
      this.cbCache.map(fn=>fn());
      //清空,下次再来
      this.cbCache = [];
    }
  }
}
// options处理,来自源码
// 深拷贝,不改变原输入
function copyObject(source) {
  var target = {};
  for (var key in source)
    target[key] = source[key];
  return target;
}

// 判断options,使options合法化,返回options
function getOptions(options, defaultOptions) {
  // 传入null或者undefined,返回默认配置defaultOptions
  if (options === null || options === undefined || typeof options === 'function') {
    return defaultOptions;
  }
  // 如果是字符串,默options认为编码
  if (typeof options === 'string') {
    // 源代码用util._extend,已废弃,用Object.assign()代替
    // 用Object.assign()强制把defaultOptions变成json格式
    // 甚至你的defaultOptions是个函数(笑
    defaultOptions = Object.assign({}, defaultOptions);
    defaultOptions.encoding = options;
    // e.g.  options.encoding = 'utf8'
    options = defaultOptions;
  } else if (typeof options !== 'object') {
    throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`);
  }
  // 如果options.encoding不是'buffer',则判断所输入encoding是否合法
  if (options.encoding !== 'buffer') assertEncoding(options.encoding);
  return options;
}
// 判断encoding是否合法的函数
function assertEncoding(encoding) {
  // Buffer.isEncoding(),存在如utf8,则返回true
  if (encoding && !Buffer.isEncoding(encoding)) {
    throw new Error(`Unknown encoding: ${encoding}`);
  }
}
module.exports = WriteStream;

但是和源码还是略有差别

可读流

可读流就简单很多
无非是触发 data 开始读
pause方法会阻断可读流
resume方法使可读流继续

let fs = require('fs');
let EventEmiter = require('events');
let first = undefined;
let firstFlag = true;
/** 
 * --------------------------------------------------
 * Node.js源代码中
 * ReadStream继承(call)Readable
 * Readable继承(call)Stream
 * Stream继承(call)EE(EventEmiter)
 * 是一个链式关系
 * 所以我们直接继承EventEmiter
 * 并且将Readable中的read等方法写在WriteStream中
 * 
 * @param {string}  path    地址
 * @param {object}  options 配置
 * @extends EventEmiter
 * @this  ReadStream
 * --------------------------------------------------
 */
class ReadStream extends EventEmiter {
  constructor(path, options) {
    super();
    // 设置options配置
    options = copyObject(getOptions(options, {}));
    // path必须是string类型
    if (typeof path !== 'string') throw Error('path must be string');
    // 地址
    this.path = path;
    // 文件标识符
    this.fd = options.fd === undefined ? null : options.fd;
    // 默认r
    this.flags = options.flags === undefined ? 'r' : options.flags;
    // 默认0o666
    this.mode = options.mode === undefined ? 0o666 : options.mode;
    // 编码 默认utf8
    this.encoding = options.encoding || 'utf8';
    // 开始点
    this.start = options.start || 0;
    // 结束点
    this.end = options.end || null;
    // 高水位线
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    // 是否自动关闭
    this.autoClose = options.autoClose === undefined ? true : options.autoClose;
    // 偏移量 初始为起始点
    this.pos = this.start;
    // 是否流动,流动就读,不流动就不读
    this.isFlow = null;
    // 创建buffer,作为存放读到的内容的容器
    this.buffer = Buffer.alloc(this.highWaterMark);
    // 打开文件
    this.open();
    // 当用户监听流data,就开始读取
    this.on('newListener', type => {
      if (type === 'data') {
        this.isFlow = true;
        this._read();
      }
    })
  }
  // 打开文件
  open() {
    fs.open(this.path, this.flags, (err, fd) => {
      // 如果报错
      if (err) {
        // 触发error事件,传入err
        this.emit('error', err);
        // 是否自动关闭文件?
        if (this.autoClose) {
          // 关闭文件
          this.destory();
        }
        return;
      }
      // 获取目标fd
      this.fd = fd;
      // 触发open事件
      this.emit('open');
    })
  }
  // 关闭文件
  destory() {
    // fd存在,表示已经打开文件
    if (typeof this.fd === 'number') {
      // 关闭文件
      fs.close(this.fd, () => {
        // 清空fd
        this.fd = null;
        // 触发close
        this.emit('close');
      });
      return;
    }
    // fd不存在,文件未打开,触发close
    this.emit('close');
  }
  // 内部读的接口
  _read() {
    // 如果还未打开
    if (typeof this.fd !== 'number') {
      this.once('open', () => {
        this._read();
      });
      return;
    }
    // 已经打开了
    // readNum是所读长度,当this.end存在,取高水位先和end到pos差两者最小,end不存在,默认最高水位线
    let readNum = this.end ? Math.min(this.highWaterMark, this.end - this.pos + 1) : this.highWaterMark;
    // 开始读
    fs.read(this.fd, this.buffer, 0, readNum, this.pos, (err, bytesHasRead) => {
      // 有内容
      if (bytesHasRead > 0) {
        // 增加偏移量
        this.pos += bytesHasRead;
        let result = this.buffer.slice(0, bytesHasRead);
        this.emit('data',
          this.encoding ? result.toString(this.encoding) : result
        );
        // 开允许读,就继续读
        if (this.isFlow) {
          this._read();
        }
      } else {
        // 没内容了 触发end
        this.emit('end');
        this.destory();
      }
    });
  }
  // 暂停!
  pause() {
    this.isFlow = false;
  }
  // 开始
  resume() {
    // 确认是否打开了文件  以免空读 报错
    if (this.fd) {
      this.isFlow = true;
      this._read();
    }
  }
}
// options处理 来自源码
function copyObject(source) {
  var target = {};
  for (var key in source)
    target[key] = source[key];
  return target;
}

function getOptions(options, defaultOptions) {
  if (options === null || options === undefined || typeof options === 'function') {
    return defaultOptions;
  }
  if (typeof options === 'string') {
    defaultOptions = Object.assign({}, defaultOptions);
    defaultOptions.encoding = options;
    options = defaultOptions;
  } else if (typeof options !== 'object') {
    throw new TypeError(`"options" must be a string or an object, got ${typeof options} instead.`);
  }
  if (options.encoding !== 'buffer') assertEncoding(options.encoding);
  return options;
}

function assertEncoding(encoding) {
  if (encoding && !Buffer.isEncoding(encoding)) {
    throw new Error(`Unknown encoding: ${encoding}`);
  }
}
module.exports = ReadStream;
let fs = require('fs');
let ReadStream = require('./myReadStream')
let rs = new ReadStream('./target.txt',{
  highWaterMark:3,
  autoClose:true,
  flags:'r',
  start:3,
  end: 14,
  encoding:'utf8'
});

rs.on('open',function () {
  console.log('open')
});
rs.on('data',function (data) {
  console.log(data);
  //暂停
  rs.pause();
});
rs.on('end',function () {
  console.log('完毕')
})
rs.on('close',function () {
  console.log('close');
  clearInterval(timer);
})
var timer = setInterval(() => {
  // 每隔300ms继续一次
  rs.resume();
}, 300);

target内容为“12345678901234567890”
输出

open
456
789
012
345
完毕
close

可读流就写好了