Stream一共有四种流的类型。分别是:
- 可读 Readable
- 可写 Writable
- 可读可写的 Duplex
- 可读可写 中间可做转换 Transform
Duplex 指的是我们既可以从流里读,也可以往流里写。这两个过程互不干扰,独立的。也就是说不能自己读到自己写的,读和写是完全分开的。
Transform 也是可以读可以写,区别是,我们往流里写入一段数据,会自动做一种转换,转换成另一种形式,再由我们读出来。就好像我们在流的入口写,在流的出口等着数据出来。即操作被写入数据,然后读出结果。
一. 可读流
可读流是对提供数据的来源的一种抽象。 可读流除了常用的data,end事件,还有一些其他事件和方法。
const stream=fs.createReadStream('./big_file.txt') 当创建了一个流的时候,就有了这个可读的流对象。然后一般监听它的data事件接收数据。那如果没监听data事件,数据会往外流吗?怎么知道数据何时才能流动呢?这就与流的状态有关了。
可读流有两种状态:静止态paused 流动态flowing 。
顾名思义,静止态时数据就无法流动了,流动态时数据才会流动。默认是处于pause态的。也就是说,只创建了这个流,它是没有流动的,处于pause态。
只有当监听了data事件,状态才会变为flowing态。数据就开始流动了。删掉了data事件监听,就变为了pause态。
调用pipe()方法将数据发送到可写流,也会将状态切换到flowing
pause()方法会让状态变为pause态。resume()方法,恢复成flowing态。
const fs=require('fs')
const stream=fs.createReadStream('./big_file.txt')
stream.on('data',chunk=>{
console.log(`接收到 ${chunk.length} 字节的数据`);
stream.pause()
console.log('暂停')
setTimeout(()=>{
console.log('数据重新开始流动')
stream.resume()
},3000)
})
这个文件很大,执行的结果就是先打印出来接收到 ${chunk.length} 字节的数据,然后打印'暂停',三秒后打印了'数据重新开始流动'。一直这样循环,知道所有数据都被读完了。
创建自定义的可读流
还是先引入stream模块的Readable类
1. 先把所有数据都push进去,再一次性读
// readable1.js
const {Readable}=require('stream')
const readStream=new Readable()
readStream.push('abcdefg')
readStream.push('hijklmn')
readStream.push(null)
readStream.on('data',(chunk)=>{
process.stdout.write(chunk)
console.log('数据读了一次')
})
第一种实现方法,就只new一个可读流对象readStream 。使用readStream.push分两次添加数据。被添加的数据会在之后触发的data事件或者read()方法时被读取。 然后监听这个可读流的data事件,把数据写到可写流process.stdout里,输出。
可以看到这个流被读了两次。因为是先把所有数据都push进去,再等着别人从流里读出来。
2. 别人调一次,才给一次数据
// readable2.js
const {Readable}=require('stream')
const readStream=new Readable({
read(size){
console.log('被读取了一次')
const char=String.fromCharCode(this.currentCharCode++)
this.push(char)
if(this.currentCharCode>90){ // 已经到Z了
this.push(null)
}
}
})
readStream.currentCharCode=65 // A
readStream.pipe(process.stdout)
和上一种不同的就是在可读流readStream里写一个read方法,这个read方法会在调用readStream.pipe(process.stdout)时,每次从readStream里读一次数据,就被调用一次,类似data事件,是在不断被触发的。
在read里,先把code转换成对应的字符,A-Z。然后push进去。加到Z时,push null,表示结束。上边说过,push的数据会在之后的read方法被调用时被读取出来。
最后把流里读到的数据通过pipe流到输出控制台。
后边一直到Z的输出没有截取。可以看到,和上一种不同的是,read方法被调用了26次。也就是说,这次的数据是按需供给的,别人读一次,我这个可读流才给一次数据。而不是先把数据给完,再等着别人读。
二. 可写流
可写流是对数据要被写入的目的地的一种抽象。 比如fs文件系统的写入流,http模块作为服务器的响应也是一种可写流。
可读流可以关注数据何时从源头流出来。可写流就不一样了,可写流是我们主动去写给别人的,所以不需要什么是否流动的状态。我们调用stream.write(data)就是开始写了,数据就自然流动了。不调用,自然就不写。
但是如果我们写的太快,或者一次写的太多,就有可能导致这个流被阻塞了,像堵车一样。这时就不可再写了。
1. drain事件
当我们调用stream.write(data) 得到的返回值是false时,就表明数据积压了,不能再写了。这时就不能再调用write了。而要监听drain事件 ,它会在可以继续写入数据时被触发。 drain就是干涸的意思,就是水流干了,可以继续加水了,可以继续写数据了。
// 向可写流中写入数据一百万次。
// 留意背压(back-pressure)。
function writeOneMillionTimes(writer, data, encoding, callback) {
let i = 1000000;
write();
function write() {
let ok = true;
do {
i--;
if (i === 0) {
// 最后一次写入。
writer.write(data, encoding, callback);
} else {
// 检查是否可以继续写入。
// 不要传入回调,因为写入还没有结束。
ok = writer.write(data, encoding);
}
} while (i > 0 && ok);
if (i > 0) {
// 被提前中止。
// 当触发 'drain' 事件时继续写入。
writer.once('drain', write);
}
}
}
2. finish事件
调用了 stream.end() 而且缓冲数据都已传给底层系统之后触发 。
因为我们向流写数据并不是直接写进去的,而是先写进缓冲区里,当缓冲区的数据达到一定量。再传给流。所以finish事件会在所有事情全部做完就触发。
3. 创建自定义的可写流
前边我们都是在调用比如fs的可写流或http的响应可写流。如何自定义一个可写流呢?自定义一个可写流就是我们自己写一个流对象,别人调用这个流对象的write方法,就能往这个流对象里写入数据。
// writable.js
const {Writable}=require('stream')
const outStream=new Writable({
write(chunk,encoding,callback){
console.log(chunk.toString())
callback() // 必须调用,才能进入下一次
}
})
process.stdin.pipe(outStream)
// pipe相当于监听前边可读流的data事件,把chunk写到后边的可写流outStream里
//process.stdin.on('data',chunk=>{
//outStream.write(chunk) 相当于调用了outStream的write方法
//})
首先从stream模块引入它的Writable类,因为可写流Writable就是stream.Writable这个类的实例。
然后new一个流对象outStream ,写一个write方法 ,该方法就是别人调用时我们该怎么做的一个函数。我们就只把数据打印出来。
最后 process.stdin是一个可读流,就是用户的输入。把用户输入的数据通过pipe流到这个可写流对象里。
当执行node writable.js 可以看到命令行在等待输入,我们输入什么,就会打印出来相同的内容:
三. 创建自定义的duplex流
就是把可读流和可写流结合起来,在流对象里同时实现一个write方法和一个read方法。让这个流既能读也能写。
const {Duplex}=require('stream')
const duplexStream=new Duplex({
write(chunk,encoding,callback){
console.log('我现在是个可写流,这是我接受的数据:')
console.log(chunk.toString())
callback()
},
read(size){
console.log('我现在是个可读流,这是我要给你的数据')
this.push(String.fromCharCode(this.currentCode++))
if(this.currentCode>67){
this.push(null)
}
}
})
duplexStream.currentCode=65
作为可写流:process.stdin.pipe(duplexStream)
process.stdin.pipe(duplexStream).pipe(process.stdout) 同时实现,在终端会先作为可读流,把ABC输出到控制台;再作为可写流,等待用户输入,调用write把输入的数据打印出来:
四. 创建自定义的transform流
transform流就是我在流的入口传数据,它先作为可写流接受数据。然后把接受的数据做一个转换。再作为可读流,将转换后的数据吐出来。我在流的出口把数据读出来。
const {Transform}=require('stream')
const upperCaseTr=new Transform({
transform(chunk,encoding,callback){
const upperChunk=chunk.toString().toUpperCase()
this.push(upperChunk)
callback()
}
})
process.stdin.pipe(upperCaseTr).pipe(process.stdout)
我们实现的转换功能是把流入的字符转成大写。
在流对象里实现一个transform方法。
- 先作为可写流,接受一个chunk,把chunk变成大写字符
- 紧接着作为可读流,调用
push方法添加数据,等待被读取
从控制台等待用户的输入,通过管道流入这个transform流对象,经过转换,再把数据流入控制台的输出。
五. 其他内置的transform Stream
1. 文件压缩
文件压缩的过程可以用流的形式实现。
const fs=require('fs')
const zlib=require('zlib')
const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')
r.pipe(zlib.createGzip()).pipe(w)
创建可读流r,可写流w,和zlib.createGzip()得到的一个Gzip对象用于压缩。从源文件一点点读,读出的内容流入Gzip流对象进行压缩(transform),再流入目标文件。就实现了文件的压缩。
2. 升级版1
const fs=require('fs')
const zlib=require('zlib')
const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')
r
.pipe(zlib.createGzip())
.on('data',()=>process.stdout.write('.'))
.pipe(w)
.on('finish',()=>console.log('done'))
添加了两个监听器。
- 每一块数据流入gzip里压缩后,监听data事件。那么一有数据被压缩完成,data事件就会被触发,它就将作为一个可读流把压缩数据吐出去。我在这个回调里往控制台输出一个
.可以用于观察这一个文件被分成了几次压缩。(一有数据,就打个点) on监听是不影响数据流动的。在每一次压缩后的数据流入新文件后,监听这个可写流的finish事件,当所有数据都传完了,被触发,打印一个标记。
所以升级后,如果正在压缩,控制台就会不断的打点。如果压缩完毕,就输出done。可以用于观察压缩进度以及何时完成。可以看到它分成了两次压缩
3. 升级版-任意变换
const fs=require('fs')
const zlib=require('zlib')
const {Transform}=require('stream')
const r=fs.createReadStream('./input.txt')
const w=fs.createWriteStream('./input.txt.gz')
const reportProgress=new Transform({
transform(chunk,encoding,callback){
process.stdout.write('.')
callback(null,chunk) // 相当于加一个this.push(chunk)都可以把数据吐出去
}
})
r
.pipe(zlib.createGzip())
.pipe(reportProgress)
.pipe(w)
.on('finish',()=>console.log('done'))
new Transform创建一个transform流对象。在对象里定义一个transform方法:一有数据进来,我就在控制台打个点,而且把数据原封不动地传出去。
数据被压缩后,通过pipe再流入这个transform流对象->流入压缩文件。
实现的效果和2相同。就是把观察进度的监听data事件换成了一个变换流。我们可以在transform方法里定义任何变换。这个例子里没有变换,直接把chunk传出去。
六. 总结
总结一下node js中都有哪些流: