node -- stream(实现流)

1,138 阅读14分钟

参考链接

概述

  • 开发者可以声明一个新的 JavaScript类并继承四个基本流类中之一(stream.Writeable、 stream.Readable、 stream.Duplex 或 stream.Transform),且确保调用了对应的父类构造器:

  • 根据所创建的流类型,新的流类必须实现一个或多个特定的方法,如下图所示:

    用例 需实现的方法
    只读流 Readable _read
    只写流 Writable _write, _writev, _final
    可读可写流 Duplex _read, _write, _writev, _final
    对写入的数据进行操作,然后读取结果 Transform _transform, _flush, _final

实现可读流

new stream.Readable([options])

options <Object>
    highWaterMark <number> 从底层资源读取数据并存储在内部缓冲区中的最大字节数。 默认为 16384 (16kb), 对象模式的流默认为 16。
    encoding <string> 如果指定了,则使用指定的字符编码将 buffer 解码成字符串。 默认为 null。
    objectMode <boolean> 流是否可以是一个对象流。 也就是说 stream.read(n) 会返回对象而不是 Buffer。 默认为 false。
    read <Function> 对 stream._read() 方法的实现。
    destroy <Function> 对 stream._destroy() 方法的实现。

readable._read(size)

size <number> 要异步读取的字节数。
  • size 是可选的参数。 对于读取是一个单一操作的实现,可以使用 size 参数来决定要读取多少数据。 对于其他的实现,可以忽略这个参数,只要有数据就提供数据。 不需要等待指定 size 字节的数据在调用 stream.push(chunk)。
  • 该函数不能被应用程序代码直接调用。 它应该由子类实现,且只能被内部的 Readable 类的方法调用。
  • 所有可读流的实现必须提供 readable._read() 方法从底层资源获取数据。
  • 当 readable._read() 被调用时,如果从资源读取到数据,则需要开始使用 this.push(dataChunk) 推送数据到读取队列。 _read() 应该持续从资源读取数据并推送数据,直到 readable.push() 返回 false。 若想再次调用 _read() 方法,则需要恢复推送数据到队列。
  • 一旦 readable._read() 被调用,它不会被再次调用,除非调用了 readable.push()。 这是为了确保 readable._read() 在同步执行时只会被调用一次。
  • readable._read() 方法有下划线前缀,因为它是在定义在类的内部,不应该被用户程序直接调用。

readable._destroy(err, callback)

_destroy() 方法会被 readable.destroy() 调用。 它可以被子类重写,但不能直接调用。
err <Error> 可能发生的错误。
callback <Function> 回调函数。

readable.push(chunk[, encoding])

chunk <Buffer> | <Uint8Array> | <string> | <null> | <any> 要推入读取队列的数据块。  
encoding 必须是有效的 Buffer 字符编码,例如 'utf8' 或 'ascii'。
返回: <boolean> 如果还有数据块可以继续推入,则返回 true,否则返回 false。
  • 当 chunk 是 Buffer、 Uint8Array 或字符串时, chunk 的数据会被添加到内部队列中供流消费。 在没有数据可写入后,给 chunk 传入 null 表示流的结束(EOF)。
  • 当可读流处在暂停模式时,使用 readable.push() 添加的数据可以在触发 'readable' 事件时通过调用 readable.read() 读取。
  • 当可读流处于流动模式时,使用 readable.push() 添加的数据可以通过触发 'data' 事件读取。

readable.push() 方法被设计得尽可能的灵活。 例如,当需要封装一个带有'暂停/继续'机制与数据回调的底层数据源时,该底层数据源可以使用自定义的可读流实例封装:

// `source` 是一个有 `readStop()` 和 `readStart()` 方法的对象,
// 当有数据时会调用 `ondata` 方法,
// 当数据结束时会调用 `onend` 方法。

class SourceWrapper extends Readable {
  constructor(options) {
    super(options);

    this._source = getLowlevelSourceObject();

    // 每当有数据时,将其推入内部缓冲。
    this._source.ondata = (chunk) => {
      // 如果 push() 返回 `false`,则停止读取。
      if (!this.push(chunk))
        this._source.readStop();
    };

    // 当读取到尽头时,推入 `null` 表示流的结束。
    this._source.onend = () => {
      this.push(null);
    };
  }
  // 当流想推送更多数据时, `_read` 会被调用。
  _read(size) {
    this._source.readStart();
  }
}
  • readable.push() 只能被可读流的实现调用,且只能在 readable._read() 方法中调用。

  • 对于非对象模式的流,如果 readable.push() 的 chunk 参数为 undefined,则它会被当成空字符串或 buffer。

读取时的异常处理

  • 当 readable._read() 在执行期间发生的错误时,建议触发 'error' 事件而不是抛出错误。 从 readable._read() 中抛出错误可能会导致无法预期的结果。 使用'error' 事件可以确保对错误进行一致且可预测的处理。
const { Readable } = require('stream');

const myReadable = new Readable({
  read(size) {
    if (checkSomeErrorCondition()) {
      process.nextTick(() => this.emit('error', err));
      return;
    }
    // 各种处理。
  }
});

实例:流式消耗迭代器中的数据。

'use strict'
const { Readable } = require('stream');

class ToReadable extends Readable {
  constructor(iterator) {
    super()
    this.iterator = iterator
  }

  // 子类需要实现该方法
  // 这是生产数据的逻辑
  _read() {
    const res = this.iterator.next()
    if (res.done) {
      // 数据源已枯竭,调用`push(null)`通知流
      return this.push(null)
    }
    setTimeout(() => {
      // 通过`push`方法将数据添加到流中
      this.push(res.value + '\n')
    }, 0)
  }
}

module.exports = ToReadable

实际使用时,new ToReadable(iterator)会返回一个可读流,下游可以流式的消耗迭代器中的数据。

const iterator = function (limit) {
  return {
    next: function () {
      if (limit--) {
        return { done: false, value: limit + Math.random() }
      }
      return { done: true }
    }
  }
}(1e10)

const readable = new ToReadable(iterator)

// 监听`data`事件,一次获取一个数据
readable.on('data', data => process.stdout.write(data))

// 所有数据均已读完
readable.on('end', () => process.stdout.write('DONE'))

执行上述代码,将会有100亿个随机数源源不断地写进标准输出流。

创建可读流时,需要继承Readable,并实现_read方法。

  • _read方法是从底层系统读取具体数据的逻辑,即生产数据的逻辑。
  • _read方法中,通过调用push(data)将数据放入可读流中供下游消耗。
  • _read方法中,可以同步调用push(data),也可以异步调用。
  • 当全部数据都生产出来后,必须调用push(null)来结束可读流。
  • 流一旦结束,便不能再调用push(data)添加数据。

可以通过监听data事件的方式消耗可读流。

  • 在首次监听其data事件后,readable便会持续不断地调用_read(),通过触发data事件将数据输出。
  • 第一次data事件会在下一个tick中触发,所以,可以安全地将数据输出前的逻辑放在事件监听后(同一个tick中)。
  • 当数据全部被消耗时,会触发end事件。

上面的例子中,process.stdout代表标准输出流,实际是一个可写流。

实现可写流

  • 前面通过继承的方式去创建一类可读流,这种方法也适用于创建一类可写流,只是需要实现的是_write(data, enc, next)方法,而不是_read()方法。

new stream.Writable([options])

options <Object>
    highWaterMark <number> 当调用 stream.write() 开始返回 false 时的缓冲大小。 默认为 16384 (16kb), 对象模式的流默认为 16。
    decodeStrings <boolean> 是否把传入 stream._write() 的字符串编码为 Buffer,使用的字符编码为调用 stream.write() 时指定的。 默认为 true。
    defaultEncoding <string> 当 stream.write() 的参数没有指定字符编码时默认的字符编码。 默认为 'utf8'。
    objectMode <boolean> 是否可以调用 stream.write(anyObj)。 一旦设为 true,则除了字符串、 Buffer 或 Uint8Array,还可以写入流实现支持的其他 JavaScript 值。 默认为 false。
    emitClose <boolean> 流被销毁后是否触发 'close' 事件。 默认为 true。
    write <Function> 对 stream._write() 方法的实现。
    writev <Function> 对 stream._writev() 方法的实现。
    destroy <Function> 对 stream._destroy() 方法的实现。
    final <Function> 对 stream._final() 方法的实现。

writable._write(chunk, encoding, callback)

chunk 要写入的数据块。 会一直是 buffer,除非 decodeStrings 选项设为 false 或者流处于对象模式。
encoding <string> 如果 chunk 是字符串,则指定字符编码。 如果 chunk 是 Buffer 或者流处于对象模式,则无视该选项。
callback <Function> 当数据块被处理完成后的回调函数。
所有可写流的实现必须提供 writable._write() 方法将数据发送到底层资源。
  • Transform 流会提供自身实现的 writable._write()。

  • 该函数不能被应用程序代码直接调用。 它应该由子类实现,且只能被内部的 Writable 类的方法调用。

  • 无论是成功完成写入还是写入失败出现错误,都必须调用 callback。 如果调用失败,则 callback 的第一个参数必须是 Error 对象。 如果写入成功,则 callback 的第一个参数为 null。

  • 在 writable._write() 被调用之后且 callback 被调用之前,所有对 writable.write() 的调用都会把要写入的数据缓冲起来。 当调用 callback 时,流将会触发 'drain'事件。 如果流的实现需要同时处理多个数据块,则应该实现 writable._writev() 方法。

  • 如果在构造函数选项中设置 decodeStrings 属性为 false,则 chunk 会保持原样传入 .write(),它可能是字符串而不是 Buffer。 这是为了实现对某些特定字符串数据编码的支持。 当 decodeStrings 为 false 时,则 encoding 参数指定字符串的字符编码。 否则,则 encoding 不起作用。

writable._writev(chunks, callback)

chunks <Object[]> 要写入的多个数据块。 每个数据块的格式为{ chunk: ..., encoding: ... }。
callback <Function> 当全部数据块被处理完成后的回调函数。
  • 该函数不能被应用程序代码直接调用。 该函数应该由子类实现,且只能被内部的 Writable 类的方法调用。

  • writable._writev() 能够同时处理多个数据块。如果实现了该方法,调用该方法时会传入当前缓冲在写入队列中的所有数据块。

  • writable._writev() 方法有下划线前缀,因为它是在定义在类的内部,不应该被用户程序直接调用。

有些简单的情况下不需要创建一类流,而只是一个流对象,可以用如下方式去做:

const Writable = require('stream').Writable

const writable = Writable()
// 实现`_write`方法
// 这是将数据写入底层的逻辑
writable._write = function (data, enc, next) {
  // 将流中的数据写入底层
  process.stdout.write(data.toString().toUpperCase())
  // 写入完成时,调用`next()`方法通知流传入下一个数据
  process.nextTick(next)
}

// 所有数据均已写入底层
writable.on('finish', () => process.stdout.write('DONE'))

// 将一个数据写入流中
writable.write('a' + '\n')
writable.write('b' + '\n')
writable.write('c' + '\n')

// 再无数据写入流时,需要调用`end`方法
writable.end()
  • 上游通过调用writable.write(data)将数据写入可写流中。write()方法会调用_write()data写入底层。 在_write中,当数据成功写入底层后,必须调用next(err)告诉流开始处理下一个数据。
  • next的调用既可以是同步的,也可以是异步的。
  • 上游必须调用writable.end(data)来结束可写流,data是可选的。此后,不能再调用write新增数据。
  • end方法调用后,当所有底层的写操作均完成时,会触发finish事件。

writable._destroy(err, callback)

_destroy() 方法会被 writable.destroy() 调用。 它可以被子类重写,但不能直接调用。
err <Error> 可能发生的错误。
callback <Function> 回调函数。

writable._final(callback)

callback <Function> 当结束写入所有剩余数据时的回调函数。
_final() 方法不能直接调用。 它应该由子类实现,且只能通过内部的 Writable 类的方法调用。
  • 该方法会在流关闭之前被调用,且在 callback 被调用后触发 'finish' 事件。 * 主要用于在流结束之前关闭资源或写入缓冲的数据。

写入时的异常处理

  • 当 writable._write() 和 writable._writev() 在执行期间发生的错误时,建议调用回调函数并传入错误对象作为第一个参数。 这样 Writable 就会触发 'error' 事件。 从 writable._write() 中抛出错误可能会导致无法预期的结果。 使用回调可以确保对错误进行一致且可预测的处理。

    const { Writable } = require('stream');
    
    const myWritable = new Writable({
    write(chunk, encoding, callback) {
        if (chunk.toString().indexOf('a') >= 0) {
          callback(new Error('无效的数据块'));
        } else {
            callback();
        }
    }
    });
    

实现双工流

  • stream.Duplex 类的原型继承自 stream.Readable 和寄生自 stream.Writable,但是 instanceof 对这两个基础类都可用,因为重写了 stream.Writable 的 Symbol.hasInstance。
  • 一个Duplex对象既可当成可读流来使用(需要实现_read方法),也可当成可写流来使用(需要实现_write方法)。
  • 双工流最重要的方面是,可读端和可写端相互独立于彼此地共存在同一个对象实例中。

new stream.Duplex(options)

options <Object> 同时传给 Writable 和 Readable 的构造函数。  * allowHalfOpen <boolean> 如果设为 false,则当可读端结束时,可写端也会自动结束。 默认为 true。

    readableObjectMode <boolean> 设置流的可读端为 objectMode。 如果 objectMode 为 true,则不起作用。 默认为 false。

    writableObjectMode <boolean> 设置流的可写端为 objectMode。 如果 objectMode 为 true,则不起作用。 默认为 false。

    readableHighWaterMark <number> 设置流的可读端的 highWaterMark。 如果已经设置了 highWaterMark,则不起作用。

    writableHighWaterMark <number> 设置流的可写端的 highWaterMark。 如果已经设置了 highWaterMark,则不起作用。
var Duplex = require('stream').Duplex

var duplex = Duplex()

// 可读端底层读取逻辑
duplex._read = function () {
  this._readNum = this._readNum || 0
  if (this._readNum > 1) {
    this.push(null)
  } else {
    this.push('' + (this._readNum++))
  }
}

// 可写端底层写逻辑
duplex._write = function (buf, enc, next) {
  // a, b
  process.stdout.write('_write ' + buf.toString() + '\n')
  next()
}

// 0, 1
duplex.on('data', data => console.log('ondata', data.toString()))

duplex.write('a')
duplex.write('b')

duplex.end()

上面的代码中实现了_read方法,所以可以监听data事件来消耗Duplex产生的数据。 同时,又实现了_write方法,可作为下游去消耗数据。

因为它既可读又可写,所以称它有两端:可写端和可读端。 可写端的接口与Writable一致,作为下游来使用;可读端的接口与Readable一致,作为上游来使用。

实现转换流

new stream.Transform([options])

options <Object> 同时传给 Writable 和 Readable 的构造函数。
    transform <Function> 对 stream._transform() 的实现。
    flush <Function> 对 stream._flush() 的实现。

transform._flush(callback)

callback <Function> 当剩余的数据被 flush 后的回调函数。
  • 该函数不能被应用程序代码直接调用。 它应该由子类实现,且只能被内部的 Readable 类的方法调用。

  • 某些情况下,转换操作可能需要在流的末尾发送一些额外的数据。 例如, zlib 压缩流时会储存一些用于优化输出的内部状态。 当流结束时,这些额外的数据需要被 flush 才算完成压缩。

  • 自定义的转换流的 transform._flush() 方法是可选的。 当没有更多数据要被消费时,就会调用这个方法,但如果是在 'end' 事件被触发之前调用则会发出可读流结束的信号。

  • 在 transform._flush() 的实现中, readable.push() 可能会被调用零次或多次。 当 flush 操作完成时,必须调用 callback 函数。

  • transform._flush() 方法有下划线前缀,因为它是在定义在类的内部,不应该被用户程序直接调用。

transform._transform(chunk, encoding, callback)

chunk <Buffer> | <string> | <any> 要转换的数据块。 chunk 总会是一个 buffer,除非 decodeStrings 选项设为 false 或者流处在对象模式。
encoding <string> 如果 chunk 是字符串,则 encoding 是字符串的字符编码。 如果 chunk 是 buffer,则 encoding 的值为 'buffer'。
callback <Function> 当 chunk 处理完成时的回调函数。
  • 该函数不能被应用程序代码直接调用。 它应该由子类实现,且只能被内部的 Readable 类的方法调用。

  • 所有转换流的实现都必须提供 _transform() 方法来接收输入并生产输出。 transform._transform() 的实现会处理写入的字节,进行一些计算操作,然后使用 readable.push() 输出到可读流。

  • transform.push() 可能会被调用零次或多次用来从每次输入的数据块产生输出,调用的次数取决需要多少数据来产生输出的结果。

  • 输入的数据块有可能不会产生任何输出。

  • 当前数据被完全消费之后,必须调用 callback 函数。 当处理输入的过程中发生出错时, callback 的第一个参数传入 Error 对象,否则传入 null。 如果 callback 传入了第二个参数,则它会被转发到 readable.push()。 就像下面的例子:

transform.prototype._transform = function(data, encoding, callback) {
  this.push(data);
  callback();
};

transform.prototype._transform = function(data, encoding, callback) {
  callback(null, data);
};
  • transform._transform() 方法有下划线前缀,因为它是在定义在类的内部,不应该被用户程序直接调用。

  • transform._transform() 不能并行调用。 流使用了队列机制,无论同步或异步的情况下,都必须先调用 callback 之后才能接收下一个数据块。

  • 在上面的例子中,可读流中的数据(0, 1)与可写流中的数据(’a’, ‘b’)是隔离开的,但在Transform中可写端写入的数据经变换后会自动添加到可读端。 Tranform继承自Duplex,并已经实现了_read和_write方法,同时要求用户实现一个_transform方法。

'use strict'

const Transform = require('stream').Transform

class Rotate extends Transform {
  constructor(n) {
    super()
    // 将字母旋转`n`个位置
    this.offset = (n || 13) % 26
  }

  // 将可写端写入的数据变换后添加到可读端
  _transform(buf, enc, next) {
    var res = buf.toString().split('').map(c => {
      var code = c.charCodeAt(0)
      if (c >= 'a' && c <= 'z') {
        code += this.offset
        if (code > 'z'.charCodeAt(0)) {
          code -= 26
        }
      } else if (c >= 'A' && c <= 'Z') {
        code += this.offset
        if (code > 'Z'.charCodeAt(0)) {
          code -= 26
        }
      }
      return String.fromCharCode(code)
    }).join('')

    // 调用push方法将变换后的数据添加到可读端
    this.push(res)
    // 调用next方法准备处理下一个
    next()
  }

}

var transform = new Rotate(3)
transform.on('data', data => process.stdout.write(data))
transform.write('hello, ')
transform.write('world!')
transform.end()

// khoor, zruog!