这几天有一个需求:解压文件的时候,为了显示解压进度,需要统计已经解压的文件字节数。
其中 unzipper 库可以把 zip 文件转换为一个流,但……这个流是个什么东西?
所以接下来进行了流的学习。
https://blog.csdn.net/qq_43067585/article/details/126095175
https://juejin.cn/post/6854573219060400141
https://nodesource.com/blog/understanding-streams-in-nodejs
个人觉得这三篇写得还是不错的,多余的概念介绍我就不多说了,上面写的很清楚了。
我主要想说一下该怎么理解 Readable、Writable、Duplex 和 Tramsform 这四种流。
如何理解流
首先,为什么要有流?
想象这么一个场景,如果想要读一个大小为 20G 的文件,很明显,直接把所有内容读取到内存中是不可取的。更好的方法是,每次读一点,再处理一点,一直循环,直到文件全部读完。
那么,就会需要一个工具帮助我们每次读一点,然后把读到的数据交给我们处理,这个工具就是流。
Readable
上面所需的工具准确来说是 Readable 流。我们可以把想要读取的数据转换为 Readable 流,再从流里面读取:
Readable 流的这个 Readable 指的是相对于使用者来说的。也就是说使用者可以从流中读取数据。但是数据本身从哪来呢?所以还需要一个方法给流提供数据,所以一个 Readable 流有两类方法:
- 向流中写数据的方法
- 从流中读取数据的方法
一般来说,1 是创建者需要关心的问题,2 是使用者需要关心的问题。
举一些具体的例子:
使用者视角
假设我是一个 Readable 流的使用者,我只关心怎么从这个流中读取数据,并不关心这个流用什么方式获取供我使用的数据。
比如说 fs 模块提供一个方法 createReadStream(),这个方法传入一个文件名,返回一个 Readable 流,你不需要关心 fs 怎么读取这个文件并转换为流,你只需要对返回的流做读取即可。
使用 Readable 流有很多方法:其中包括
- 监听 data 事件,当获取数据时触发,在回调函数中处理数据 chunk
- 监听 readable 事件,手动调用 Readable 流的 read() 方法读取数据并处理
- 使用 pipe() 方法把数据传到另一个流
创建者视角
假设我是一个 Readable 流的创建者,我只需要关心该怎么把数据写入到流中,并不关心这个流被怎么使用。
比如说我有一个很长的字符串,我希望这个字符串可以被转换成一个 Readable 流,那我就需要去创建一个新的 Readable 流,并且定义这个 Readable 流怎么获取这个字符串。
重写一个 Readable 的 _read() 方法可以定义怎么把数据写入到 Readable 流中。
上面的例子意思是:定义一个新的 Readable 流,每次都读 5 个字符,然后调用 this.push() 放入流中,读完时,调用 this.push(null) 示意流结束。
总结
可以看出,Readable 流的 “readable” 是相对于使用者来说的,使用者从流中读取数据
但是对于创建者来说,要做的事情反而是相反的:创建者需要定义数据怎么写入流。这样一来,Readable 对于创建者来说反而是一个需要 write 的东西了。很神奇的视角转换。
Writable
和 Readable 类似,Writable 的 “writable” 也是相对于使用者来说的。
使用者:关心数据怎么写入到流中
创建者:关心怎么处理流提供的数据
使用者视角
还是以文件为例,fs.createWriteStream() 创建一个 Writable 流。是这样的,Writable 流只需要考虑怎么把数据提供给下游就行了,使用者把数据写入流要考虑的可就多了。
Writable 流通常提供两个方法供使用者使用:write() 和 end()。
调用 ws.write() 往流中写入数据,的返回值是 boolean 类型的,如果返回 true,则说明流的缓冲区还可以再写入数据,如果返回 false,则说明流的缓冲区满了,不能再写入数据了。
当流发出 ‘drain’ 事件时,说明流的缓冲区又空了,又可以把数据写入到流中了。
调用 ws.end() 往流中写入最后一份数据,说明这次写入之后再也不会有数据写入了。
上面的代码就是按照这个思路,把一个字符串每次写入 5 个字符到流中。
创建者视角
现在来尝试定义一个 fs.createWriteStream 返回的 Writable 流,也就是把流中数据写入到文件的 Writable 流。
需要定义 _write() 方法,第一个参数是流提供的数据块,第二个参数是数据块的编码格式,第三个参数是数据处理完成后要执行的回调。
上面的 Writable 流通过 fs.appendFile 方法,把从流中读取的数据 chunk 写入到了文件中,并且通过传递回调函数的方式使得在数据处理完成后能正确调用回调函数。
Duplex
Duplex 流是一个全双工流,可以理解为同时有一个 Readable 流和一个 Writable 流,但是两者的缓冲区是完全独立的。Socket 就是一个典型的 Duplex 流。
因为我没怎么了解这个,也还没怎么用过,这里就不多说了。
Transform
Tranform 流的作用就和它的名字一样,是用来转换数据的,其作用相当于一个适配器。