流的分类
var Stream = require('stream')
var Readable = Stream.Readable
var Writable = Stream.Writable
var Duplex = Stream.Duplex
var Transform = Stream.Transform
- Readable - 可读的流 (例如
fs.createReadStream()
). - Writable - 可写的流 (例如
fs.createWriteStream()
). - Duplex - 可读写的流 (例如
net.Socket
). - Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如
zlib.createDeflate()
).
一、Stream.Readable 类
三种状态
-
可读流对象
readable
中有一个维护状态的对象,readable._readableState
,这里简称为state
。 其中有一个标记,state.flowing
, 可用来判别流的模式。 它有三种可能值:readable._readableState.flowing = null
:由于不存在数据消费者,可读流将不会产生数据。 在这个状态下,监听 'data' 事件,调用 readable.pipe() 方法,或者调用 readable.resume() 方法, readable._readableState.flowing 的值将会变为 true 。这时,随着数据生成,可读流开始频繁触发事件。readable._readableState.flowing = false
:调用 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背压”(back pressure), 将导致 readable._readableState.flowing 值变为 false。 这将暂停事件流,但 不会 暂停数据生成。 在这种情况下,为 'data' 事件设置监听函数不会导致 readable._readableState.flowing 变为 true。readable._readableState.flowing = true
对于大多数用户,建议使用readable.pipe()
方法来消费流数据,因为它是最简单的一种实现。开发者如果要精细地控制数据传递和产生的过程,可以使用EventEmitter
和readable.pause()
/readable.resume()
提供的 API
两种模式
- flowing 模式下, 可读流自动从系统底层读取数据,并通过
EventEmitter
接口的事件尽快将数据提供给应用。 - paused 模式下,必须显式调用
stream.read()
方法来从流中读取数据片段。
所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:
- 监听
data
事件。 - 调用
stream.resume()
方法。 - 调用
stream.pipe()
方法将数据发送到 Writable。
可读流可以通过下面途径切换到 paused 模式:
- 如果不存在管道目标(pipe destination),可以通过调用
stream.pause()
方法实现。 - 如果存在管道目标,可以通过取消
'data'
事件监听,并调用stream.unpipe()
方法移除所有管道目标来实现。
这里需要记住的重要概念就是,可读流需要先为其提供消费或忽略数据的机制,才能开始提供数据。如果消费机制被禁用或取消,可读流将尝试停止生成数据。
为了向后兼容,取消 data事件监听并不会 自动将流暂停。同时,如果存在管道目标(pipe destination),且目标状态变为可以接收数据,调用了stream.pause()方法也并不保证流会一直保持暂停状态。
如果Readable切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了`readable.resume()`方法却没有监听data事件,或是取消了data事件监听,就有可能出现这种情况。
暂停模式
- 在初始状态下,监听
data
事件,会使流进入流动模式。 - 但如果在暂停模式下,监听
data
事件并不会使它进入流动模式。 - 为了消耗流,需要显示调用
read()
方法。
const Readable = require('stream').Readable
// 底层数据
const dataSource = ['a', 'b', 'c']
const readable = Readable()
readable._read = function () {
if (dataSource.length) {
this.push(dataSource.shift())
} else {
this.push(null)
}
}
// 进入暂停模式
readable.pause()
readable.on('data', data => process.stdout.write('\ndata: ' + data))
var data = readable.read()
while (data !== null) {
process.stdout.write('\nread: ' + data)
data = readable.read()
}
执行上面的脚本,输出如下:
data: a
read: a
data: b
read: b
data: c
read: c
可见,在暂停模式下,调用一次read
方法便读取一次数据。
执行read()
时,如果缓存中数据不够,会调用_read()
去底层取。
_read
方法中可以同步或异步地调用push(data)
来将底层数据交给流处理。
在上面的例子中,由于是同步调用push
方法,数据会添加到缓存中。
read
方法在执行完_read
方法后,便从缓存中取数据,再返回,且以data
事件输出。
如果改成异步调用push
方法,则由于_read()
执行完后,数据来不及放入缓存,
将出现read()
返回null
的现象。
见下面的示例:
const Readable = require('stream').Readable
// 底层数据
const dataSource = ['a', 'b', 'c']
const readable = Readable()
readable._read = function () {
process.nextTick(() => {
if (dataSource.length) {
this.push(dataSource.shift())
} else {
this.push(null)
}
})
}
readable.pause()
readable.on('data', data => process.stdout.write('\ndata: ' + data))
while (null !== readable.read()) ;
执行上述脚本,可以发现没有任何数据输出。
此时,需要使用readable
事件:
const Readable = require('stream').Readable
// 底层数据
const dataSource = ['a', 'b', 'c']
const readable = Readable()
readable._read = function () {
process.nextTick(() => {
if (dataSource.length) {
this.push(dataSource.shift())
} else {
this.push(null)
}
})
}
readable.pause()
readable.on('data', data => process.stdout.write('\ndata: ' + data))
readable.on('readable', function () {
while (null !== readable.read()) ;;
})
输出:
data: a
data: b
data: c
-
当
read()
返回null
时,意味着当前缓存数据不够,而且底层数据还没加进来(异步调用push()
)。 此种情况下state.needReadable
会被设置为true
。push
方法被调用时,由于是暂停模式,不会立即输出数据,而是将数据放入缓存,并触发一次readable
事件。 -
所以,一旦
read
被调用,上面的例子中就会形成一个循环:readable
事件导致read
方法调用,read
方法又触发readable
事件。 -
首次监听
readable
事件时,还会触发一次read(0)
的调用,从而引起_read
和push
方法的调用,从而启动循环。 -
总之,在暂停模式下需要使用
readable
事件和read
方法来消耗流。
流动模式
- 一般创建流后,监听
data
事件,或者通过pipe
方法将数据导向另一个可写流,即可进入流动模式开始消耗数据。 尤其是pipe
方法中还提供了back pressure机制,所以使用pipe
进入流动模式的情况非常普遍。 - 本节解释
data
事件如何能触发流动模式。 - 先看一下
Readable
是如何处理data
事件的监听的:
Readable.prototype.on = function (ev, fn) {
var res = Stream.prototype.on.call(this, ev, fn)
if (ev === 'data' && false !== this._readableState.flowing) {
this.resume()
}
// 处理readable事件的监听
// 省略
return res
}
Stream
继承自EventEmitter
,且是Readable
的父类。
从上面的逻辑可以看出,在将fn
加入事件队列后,如果发现处于非暂停模式,则会调用this.resume()
,开始流动模式。
resume()
方法先将state.flowing
设为true
,
然后会在下一个tick中执行flow
,试图将缓存读空:
if (state.flowing) do {
var chunk = stream.read()
} while (null !== chunk && state.flowing)
flow
中每次read()
都可能触发push()
的调用,
而push()
中又可能触发flow()
或read()
的调用,
这样就形成了数据生生不息的流动。
其关系可简述为:
下面再详细看一下push()
的两个分支:
if (state.flowing && state.length === 0 && !state.sync) {
stream.emit('data', chunk)
stream.read(0)
} else {
state.length += state.objectMode ? 1 : chunk.length
state.buffer.push(chunk)
if (state.needReadable)
emitReadable(stream)
}
称第一个分支为立即输出。
在立即输出的情况下,输出数据后,执行read(0)
,进一步引起_read()
和push()
的调用,从而使数据源源不断地输出。
在非立即输出的情况下,数据先被添加到缓存中。 此时有两种情况:
state.length
为0。 这时,在调用_read()
前,state.needReadable
就会被设为true
。 因此,一定会调用emitReadable()
。 这个方法会在下一个tick中触发readable
事件,同时再调用flow()
,从而形成流动。state.length
不为0。 由于流动模式下,每次都是从缓存中取第一个元素,所以这时read()
返回值一定不为null
。 故flow()
中的循环还在继续。
此外,从push()
的两个分支可以看出来,如果state.flowing
设为false
,第一个分支便不会再进去,也就不会再调用read(0)
。
同时第二个分支中引发flow
的调用后,也不会再调用read()
,这就完全暂停了底层数据的读取。
事实上,pause
方法就是这样使流从流动模式转换到暂停模式的。
如何通过流取到数据
-
用
Readable
创建对象readable
后,便得到了一个可读流。 -
如果实现
_read
方法,就将流连接到一个底层数据源。 流通过调用_read
向底层请求数据,底层再调用流的push
方法将需要的数据传递过来。 -
当
readable
连接了数据源后,下游便可以调用readable.read(n)
向流请求数据,同时监听readable
的data
事件来接收取到的数据。 -
这个流程可简述为:
read
read
方法中的逻辑可用下图表示,后面几节将对该图中各环节加以说明。
push方法
-
消耗方调用
read(n)
促使流输出数据,而流通过_read()
使底层调用push
方法将数据传给流。 -
如果流在流动模式下(
state.flowing
为true
)输出数据,数据会自发地通过data
事件输出,不需要消耗方反复调用read(n)
。 -
如果调用
push
方法时缓存为空,则当前数据即为下一个需要的数据。 这个数据可能先添加到缓存中,也可能直接输出。 执行read
方法时,在调用_read
后,如果从缓存中取到了数据,就以data
事件输出。 -
所以,如果
_read
异步调用push
时发现缓存为空,则意味着当前数据是下一个需要的数据,且不会被read
方法输出,应当在push
方法中立即以data
事件输出。 -
因此,上图中“立即输出”的条件是:
state.flowing && state.length === 0 && !state.sync
end事件
-
由于流是分次向底层请求数据的,需要底层显示地告诉流数据是否取完。 所以,当某次(执行
_read()
)取数据时,调用了push(null)
,就意味着底层数据取完。 此时,流会设置state.ended
。 -
state.length
表示缓存中当前的数据量。 只有当state.length
为0
,且state.ended
为true
,才意味着所有的数据都被消耗了。 一旦在执行read(n)
时检测到这个条件,便会触发end
事件。 当然,这个事件只会触发一次。
readable事件
-
在调用完
_read()
后,read(n)
会试着从缓存中取数据。 如果_read()
是异步调用push
方法的,则此时缓存中的数据量不会增多,容易出现数据量不够的现象。 -
如果
read(n)
的返回值为null
,说明这次未能从缓存中取出所需量的数据。 此时,消耗方需要等待新的数据到达后再次尝试调用read
方法。 -
在数据到达后,流是通过
readable
事件来通知消耗方的。 在此种情况下,push
方法如果立即输出数据,接收方直接监听data
事件即可,否则数据被添加到缓存中,需要触发readable
事件。 -
消耗方必须监听这个事件,再调用
read
方法取得数据。
doRead
- 流中维护了一个缓存,当缓存中的数据足够多时,调用
read()
不会引起_read()
的调用,即不需要向底层请求数据。 - 用
doRead
来表示read(n)
是否需要向底层取数据,其逻辑为:
var doRead = state.needReadable
if (state.length === 0 || state.length - n < state.highWaterMark) {
doRead = true
}
if (state.ended || state.reading) {
doRead = false
}
if (doRead) {
state.reading = true
state.sync = true
if (state.length === 0) {
state.needReadable = true
}
this._read(state.highWaterMark)
state.sync = false
}
-
state.reading
标志上次从底层取数据的操作是否已完成。 一旦push
方法被调用,就会设置为false
,表示此次_read()
结束。 -
state.highWaterMark
是给缓存大小设置的一个上限阈值。 如果取走n
个数据后,缓存中保有的数据不足这个量,便会从底层取一次数据。
howMuchToRead
调用read(n)
去取n
个数据时,m = howMuchToRead(n)
是将从缓存中实际获取的数据量。
根据以下几种情况赋值,一旦确定则立即返回:
state.length
为0,state.ended
为true
。 数据源已枯竭,且缓存为空,无数据可取,m
为0.state.objectMode
为true
。n
为0,则m
为0; 否则m
为1,将缓存的第一个元素输出。n
是数字。 若n <= 0
,则m
为0; 若n > state.length
,表示缓存中数据量不够。 此时如果还有数据可读(state.ended
为false
),则m
为0,同时设置state.needReadable
,下次执行read()
时doRead
会为true
,将从底层再取数据。 如果已无数据可读(state.ended
为true
),则m
为state.length
,将剩下的数据全部输出。 若0 < n <= state.length
,则缓存中数据够用,m
为n
。- 其它情况。
state.flowing
为true
(流动模式),则m
为缓存中第一个元素(Buffer
)的长度,实则还是将第一个元素输出; 否则m
为state.length
,将缓存读空。
上面的规则中:
-
n
通常是undefined
或0
,即不指定读取的字节数。 -
read(0)
不会有数据输出,但从前面对doRead
的分析可以看出,是有可能从底层读取数据的。 -
执行
read()
时,由于流动模式下数据会不断输出,所以每次只输出缓存中第一个元素输出,而非流动模式则会将缓存读空。 -
objectMode
为true
时,m
为0
或1
。此时,一次push()
对应一次data
事件。 -
综上所述:
-
可读流是获取底层数据的工具,消耗方通过调用
read
方法向流请求数据,流再从缓存中将数据返回,或以data
事件输出。 -
如果缓存中数据不够,便会调用
_read
方法去底层取数据。 -
该方法在拿到底层数据后,调用
push
方法将数据交由流处理(立即输出或存入缓存)。 -
可以结合
readable
事件和read
方法来将数据全部消耗,这是暂停模式的消耗方法。 -
但更常见的是在流动模式下消耗数据,具体见后面的章节。
背压反馈机制
考虑下面的例子:
const fs = require('fs')
fs.createReadStream(file).on('data', doSomething)
监听data
事件后文件中的内容便立即开始源源不断地传给doSomething()
。
如果doSomething
处理数据较慢,就需要缓存来不及处理的数据data
,占用大量内存。
理想的情况是下游消耗一个数据,上游才生产一个新数据,这样整体的内存使用就能保持在一个水平。
Readable
提供pipe
方法,用来实现这个功能。
pipe
用pipe
方法连接上下游:
const fs = require('fs')
fs.createReadStream(file).pipe(writable)
-
writable
是一个可写流Writable
对象,上游调用其write
方法将数据写入其中。 -
writable
内部维护了一个写队列,当这个队列长度达到某个阈值(state.highWaterMark
)时, 执行write()
时返回false
,否则返回true
。 -
于是上游可以根据
write()
的返回值在流动模式和暂停模式间切换:readable.on('data', function (data) { if (false === writable.write(data)) { readable.pause() } }) writable.on('drain', function () { readable.resume() })
上面便是pipe
方法的核心逻辑。
当write()
返回false
时,调用readable.pause()
使上游进入暂停模式,不再触发data
事件。
但是当writable
将缓存清空时,会触发一个drain
事件,再调用readable.resume()
使上游进入流动模式,继续触发data
事件。
看一个例子:
const stream = require('stream')
var c = 0
const readable = stream.Readable({
highWaterMark: 2,
read: function () {
process.nextTick(() => {
var data = c < 6 ? String.fromCharCode(c + 65) : null
console.log('push', ++c, data)
this.push(data)
})
}
})
const writable = stream.Writable({
highWaterMark: 2,
write: function (chunk, enc, next) {
console.log('write', chunk)
}
})
readable.pipe(writable)
输出:
push 1 A
write <Buffer 41>
push 2 B
push 3 C
push 4 D
- 虽然上游一共有6个数据(
ABCDEF
)可以生产,但实际只生产了4个(ABCD
)。 这是因为第一个数据(A
)迟迟未能写完(未调用next()
),所以后面通过write
方法添加进来的数据便被缓存起来。 - 下游的缓存队列到达2时,
write
返回false
,上游切换至暂停模式。 此时下游保存了AB
。 由于Readable
总是缓存state.highWaterMark
这么多的数据,所以上游保存了CD
。 从而一共生产出来ABCD
四个数据。
下面使用tick-node将Readable
的debug信息按tick分组:
⌘ NODE_DEBUG=stream tick-node pipe.js
STREAM 18930: pipe count=1 opts=undefined
STREAM 18930: resume
---------- TICK 1 ----------
STREAM 18930: resume read 0
STREAM 18930: read 0
STREAM 18930: need readable false
STREAM 18930: length less than watermark true
STREAM 18930: do read
STREAM 18930: flow true
STREAM 18930: read undefined
STREAM 18930: need readable true
STREAM 18930: length less than watermark true
STREAM 18930: reading or ended false
---------- TICK 2 ----------
push 1 A
STREAM 18930: ondata
write <Buffer 41>
STREAM 18930: read 0
STREAM 18930: need readable true
STREAM 18930: length less than watermark true
STREAM 18930: do read
---------- TICK 3 ----------
push 2 B
STREAM 18930: ondata
STREAM 18930: call pause flowing=true
STREAM 18930: pause
STREAM 18930: read 0
STREAM 18930: need readable true
STREAM 18930: length less than watermark true
STREAM 18930: do read
---------- TICK 4 ----------
push 3 C
STREAM 18930: emitReadable false
STREAM 18930: emit readable
STREAM 18930: flow false
---------- TICK 5 ----------
STREAM 18930: maybeReadMore read 0
STREAM 18930: read 0
STREAM 18930: need readable false
STREAM 18930: length less than watermark true
STREAM 18930: do read
---------- TICK 6 ----------
push 4 D
---------- TICK 7 ----------
- TICK 0:
readable.resume()
- TICK 1:
readable
在流动模式下开始从底层读取数据 - TICK 2:
A
被输出,同时执行readable.read(0)
。 - TICK 3:
B
被输出,同时执行readable.read(0)
。writable.write('B')
返回false
。 执行readable.pause()
切换至暂停模式。 - TICK 4: TICK 3中
read(0)
引起push('C')
的调用,C
被加到readable
缓存中。 此时,writable
中有A
和B
,readable
中有C
。 这时已在暂停模式,但在readable.push('C')
结束前,发现缓存中只有1个数据,小于设定的highWaterMark
(2),故准备在下一个tick再读一次数据。 - TICK 5: 调用
read(0)
从底层取数据。 - TICK 6:
push('D')
,D
被加到readable
缓存中。 此时,writable
中有A
和B
,readable
中有C
和D
。readable
缓存中有2个数据,等于设定的highWaterMark
(2),不再从底层读取数据。
可以认为,随着下游缓存队列的增加,上游写数据时受到的阻力变大。 这种back pressure大到一定程度时上游便停止写,等到back pressure降低时再继续
消耗驱动的数据生产
-
使用pipe()时,数据的生产和消耗形成了一个闭环。
-
通过负反馈调节上游的数据生产节奏,事实上形成了一种所谓的拉式流(pull stream)。
-
用喝饮料来说明拉式流和普通流的区别的话,普通流就像是将杯子里的饮料往嘴里倾倒,动力来源于上游,数据是被推往下游的;拉式流则是用吸管去喝饮料,动力实际来源于下游,数据是被拉去下游的。
-
所以,使用拉式流时,是“按需生产”。 如果下游停止消耗,上游便会停止生产。所有缓存的数据量便是两者的阈值和。
-
当使用Transform作为下游时,尤其需要注意消耗。
const stream = require('stream')
var c = 0
const readable = stream.Readable({
highWaterMark: 2,
read: function () {
process.nextTick(() => {
var data = c < 26 ? String.fromCharCode(c++ + 97) : null
console.log('push', data)
this.push(data)
})
}
})
const transform = stream.Transform({
highWaterMark: 2,
transform: function (buf, enc, next) {
console.log('transform', buf)
next(null, buf)
}
})
readable.pipe(transform)
以上代码执行结果为:
push a
transform <Buffer 61>
push b
transform <Buffer 62>
push c
push d
push e
push f
-
可见,并没有将26个字母全生产出来。
-
Transform中有两个缓存:可写端的缓存和可读端的缓存。
-
调用transform.write()时,如果可读端缓存未满,数据会经过变换后加入到可读端的缓存中。
-
当可读端缓存到达阈值后,再调用transform.write()则会将写操作缓存到可写端的缓存队列。
-
当可写端的缓存队列也到达阈值时,transform.write()返回false,上游进入暂停模式,不再继续transform.write()
-
所以,上面的transform中实际存储了4个数据,ab在可读端(经过了_transform的处理),cd在可写端(还未经过_transform处理)。
-
此时,由前面一节的分析可知,readable将缓存ef,之后便不再生产数据。
-
这三个缓存加起来的长度恰好为6,所以一共就生产了6个数据。
-
要想将26个数据全生产出来,有两种做法。 第一种是消耗transform中可读端的缓存,以拉动上游的生产:
readable.pipe(transform).pipe(process.stdout)
- 第二种是,不要将数据存入可读端中,这样可读端的缓存便会一直处于数据不足状态,上游便会源源不断地生产数据:
const transform = stream.Transform({
highWaterMark: 2,
transform: function (buf, enc, next) {
next()
}
})
注意、objectMode
-
前面几节的例子中,经常看到调用data.toString()。这个toString()的调用是必需的吗? 本节介绍完如何控制流中的数据类型后,自然就有了答案。
-
在shell中,用管道(|)连接上下游。上游输出的是文本流(标准输出流),下游输入的也是文本流(标准输入流)。在本文介绍的流中,默认也是如此。
-
对于可读流来说,push(data)时,data只能是String或Buffer类型,而消耗时data事件输出的数据都是Buffer类型。对于可写流来说,write(data)时,data只能是String或Buffer类型,_write(data)调用时传进来的data都是Buffer类型。
-
也就是说,流中的数据默认情况下都是Buffer类型。产生的数据一放入流中,便转成Buffer被消耗;写入的数据在传给底层写逻辑时,也被转成Buffer类型。
-
但每个构造函数都接收一个配置对象,有一个objectMode的选项,一旦设置为true,就能出现“种瓜得瓜,种豆得豆”的效果。 Readable未设置objectMode时:
const Readable = require('stream').Readable
const readable = Readable()
readable.push('a')
readable.push('b')
readable.push(null)
readable.on('data', data => console.log(data))
输出:
<Buffer 61>
<Buffer 62>
- Readable设置objectMode后:
const Readable = require('stream').Readable
const readable = Readable({ objectMode: true })
readable.push('a')
readable.push('b')
readable.push({})
readable.push(null)
readable.on('data', data => console.log(data))
输出:
a
b
{}
- 可见,设置objectMode后,push(data)的数据被原样地输出了。此时,可以生产任意类型的数据。
事件
- close: 在流或其底层资源(比如一个文件)关闭后触发,不是所有Readable都会触发
- data(chunk): 会在流将数据传递给消费者时触发。
- end: 事件只有在数据被完全消费后 才会触发
- error(err) 在底层系统内部出错从而不能产生数据,或当流的实现试图传递错误数据时发生。
- pause 当调用 stream.pause() 并且 readsFlowing 不为 false 时,就会触发 'pause' 事件。
- readable: 表明流有新的动态:要么有新的数据,要么到达流的尽头
属性
- destroyed 在调用 readable.destroy() 之后为 true。
方法
-
read([size]): 从内部缓冲区中抽出并返回一些数据。一般来说,建议开发人员避免使用'readable'事件和readable.read()方法,使用readable.pipe()或'data'事件代替。
-
isPaused(): 返回可读流的当前操作状态。
-
pause(): 方法将会使 flowing 模式的流停止触发 'data'`事件, 进而切出 flowing 模式。任何可用的数据都将保存在内部缓存中。
-
resume(): 方法会重新触发
'data'
事件, 将暂停模式切换到流动模式。 -
pipe(destination[, options])- 绑定一个 Writable 到
readable
上,将可写流自动切换到 flowing 模式并将所有数据传给绑定的 Writable。数据流将被自动管理。这样,即使是可读流较快,目标可写流也不会超负荷overwhelme。- options end 终止写入器。默认值: true。
-
unpipe([destination]): 将之前通过
stream.pipe()
方法绑定的流分离 -
setEncoding(encoding): 要使用的编码
-
unshift(chunk,[, encoding]): 把一块数据压回到Buffer内部。
-
destroy([error]): 销毁流,并且触发error事件。然后,可读流将释放所有的内部资源。
二、stream.Writable 类
事件
-
close: 件将在流或其底层资源(比如一个文件)关闭后触发。
-
drain: 如果调用
stream.write(chunk)
方法返回false
,'drain'
事件会在适合恢复写入数据到流的时候触发。 -
error(err): 写入数据出错或者使用管道出错时触发,
error
事件发生时,流并不会关闭。 -
finish: 在调用了
stream.end()
方法,且缓冲区数据都已经传给底层系统(underlying system)之后,'finish'
事件将被触发 -
pipe: 在可读流(readable stream)上调用
stream.pipe()
方法,并在目标流向 (destinations) 中添加当前可写流 ( writable ) 时 -
unpipe: 在 Readable上调用
stream.unpipe()
方法,从目标流向中移除当前 Writable时
属性
- writableLength: 返回构造该可写流时传入的 highWaterMark 参数值。
- writableHighWaterMark: 包含了写入就绪队列的字节(或者对象)数,这个值提供了关于highWaterMark状 态的内省数据。
- destroyed
- writableEnded
- writableFinished
- writableHighWaterMark
- writableObjectMode
方法
-
cork(): 强制所有写入数据都存放到内存中的缓冲区里。 直到调用
stream.uncork()
或stream.end()
方法时,缓冲区里的数据才会被输出。 -
uncork():
-
end([chunk][, encoding][, callback]): 表明接下来没有数据要被写入 Writable,如果传入了可选的
callback
函数,它将作为'finish'
事件的回调函数。 -
setDefaultEncoding(encoding):
-
write(chunk[, encoding][, callback]):
chunk
| | | 要写入的数据。可选的。 对于非对象模式下的流,chunk
必须是字符串,Buffer
或者Uint8Array
。对于对象模式下的流,chunk
可以是除null
外的任意 JavaScript 值。encoding
如果chunk
是字符串,这里指定字符编码callback
缓冲数据输出时的回调函数- 返回: 如果流需要等待
'drain'
事件触发才能继续写入数据,这里将返回false
; 否则返回true
。
-
destroy([error]): 摧毁这个流,并发出传过来的错误。当这个函数被调用后,这个写入流就结束了
三、stream.Duplex 类
Duplex 流是同时实现了 Readable和 Writable接口的流。
四、stream.Transform 类
变换流(Transform streams) 是一种 Duplex流。它的输出与输入是通过某种方式关联的。和所有 Duplex流一样,变换流同时实现了 Readable和 Writable接口。
变换流的实例包括:
- zlib streams
- crypto streams
在上面的例子中,可读流中的数据(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!
五、其他
stream.finished(stream[, options], callback)
当流不再可读、可写、发生错误、或提前关闭时,通过该函数获得通知。
const { finished } = require('stream');
const rs = fs.createReadStream('archive.tar');
finished(rs, (err) => {
if (err) {
console.error('流发生错误', err);
} else {
console.log('流已读取完');
}
});
rs.resume(); // 开始读取流。
stream.pipeline(...streams, callback)
使用管道连接多个流,并传递错误与完成清理工作,当管道连接完成时通知回调函数。
const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');
// 使用 pipeline 接口连接多个流,并在管道连接完成时获得通知。
// 使用 pipeline 可以高效地压缩一个可能很大的 tar 文件:
pipeline(
fs.createReadStream('archive.tar'),
zlib.createGzip(),
fs.createWriteStream('archive.tar.gz'),
(err) => {
if (err) {
console.error('管道连接失败', err);
} else {
console.log('管道连接成功');
}
}
);