这里记录工作中遇到的技术点,以及自己对生活的一些思考,周三或周五发布。
封面图
Node中的streams流
streams流是Node中的最好的特性之一。它在我们的开发过程当中可以帮助我们做很多事情。比如通过流的方式梳理大量数据,或者帮我们分离应用程序。
和streams流相关的内容有哪些呢?大致有这么几点:
- 处理大量数据
- 使用管道方法
- 转换流
- 读写流
- 解耦I/O
处理无限量的数据
使用data事件,我们可以在消耗很少内存的情况下去处理一小块文件。
我们其实也可以处理无限量的数据,比如:我们可以从伪随机数生成器中读取字节数。
const rs = fs.createReadStream('/dev/urandom')
let size = 0
rs.on('data', (data) => {
size += data.length
console.log('文件大小:', size)
})
结果如下:
尽管程序一直在执行,并且文件的数量也是无限的,但是程序并没有崩溃。
可伸缩性是流的特性之一,大多数使用流编写的程序都可以很好的伸缩任何输入的大小。
flow模式(flow Mode)与pull-base模式(pull-based stream)
Node中的流可以处于流模式或者pull-based模式。当我们将数据添加到流,它就进入flow模式,这表示:只要有数据,就会调用data事件。
在上面的示例代码中,readStream刚刚创建的时候,并不处于flow模式,我们通过data事件将它放置到flow模式。
如果我们想停止它,我们可以调用可读流的暂停方法pause()。如果我们想重新开启它,我们可以调用resume()方法。
但是flow模式也可能会有问题,因为在某些情况下,即使流暂停,流也可能被传入数据的淹没,传入流可能不收pause()方法控制。
从流中提取数据的另一种方法是等待readable事件,然后不断调用流的read方法,直到返回null(即流终止符实体)。通过这种方式,我们可以从流中提取数据,并且可以在必要时停止提取。
换句话说,我们不需要指示流暂停然后继续;我们可以根据需要启动或者停止它。
看个例子:
const fs = require('fs')
const rs = fs.createReadStream(__filename)
rs.on('readable', () => {
let data = rs.read()
while (data !== null) {
console.log('读取的数据:', data)
data = rs.read()
}
})
rs.on('end', () => {
console.log('没有数据了')
})
这个例子我们通过readable事件去判断是否有数据,而不是直接调用data事件。
当然,从流中提取数据更好的方法是通过pipe(管道)将我们的数据传输到我们创建的流中。这样一来管理内存的问题就可以在内部进行。
理解stream流的事件
所有流都继承自EventEmitter类并带有一系列不同的事件。了解一些我们经常用的事件,对于我们在处理流的过程当中非常有用。
第一,data事件。从可读流中读取新数据时触发。data数据作为事件处理程序的第一个参数。需要注意的是,与其他事件处理程序不同,附加数据侦听器会产生副作用。当连接第一个数据侦听器时,我们的流将被取消暂停。
第二,end事件。当可读流中没有数据时触发。
第三,finish事件。当可写流结束且所有挂起的写入都已完成时发出。
第四,close事件。通常在流完全关闭时发出,stream不一定会触发事件。
第五,puse事件。用于暂停一个可读流。大部分情况我们可以忽略这个方法。
第六,resume事件。用于重启一个可读流。
pipe方法
pipe方法用来将两个stream连接到一起。shell脚本中我们经常使用 | 管道符号来实现这个功能。通过这些方式,我们可以将多个管道连接在一起,更加方便的处理数据。
Streams的API 也为我们提供了pipe方法。每个可读都有一个pipe方法。
我们还是用一个例子来感受一下:
const zlib = require('zlib')
const map = require('tar-map-stream')
const decompress = zlib.createGunzip()
const whoami = process.env.USER ||process.env.USERNAME
const convert = map((header)=>{
header.uname = whoami
header.mtime = new Date()
header.name = header.name.replace('node-v0.1.100','edon-v0.0.0')
return header
})
const compress = zlib.createGzip()
process.stdin.pipe(decompress)
.pipe(convert)
.pipe(compress)
.pipe(process.stdout)
然后执行:
curl https://nodejs.org/dist/v0.1.100/node-v0.100.tar.gz | node read.js > edon.tar.gz
pipe方法将数据侦听绑定到streams流的源头,然后将接收到的数据导流到目标streams中。
当我们通过pipe将多个streams串联在一起时,我们是实际在告诉Node用这些流来解析数据。
使用pipe管道处理数据,比使用data方法相对来说更加安全一些,因为它可以自由的处理背压(backpressure),背压这个概念我们可以理解为内存管理。
比如,当快速生成数据的流可能会压到较慢的写入流时,需要使用缓冲压力策略来防止内存填满和进程崩溃。管道方法提供了这种背压。
上面的代码中,我们通过 | 管道符号将请求的数据导流到我们的 index.js 脚本中。
然后process.stdin标准I/O通过pipe方法一层一层的往下传递,最终通过重定向>
存入edon.tar.gz文件中。
整个过程如下:
curl---> process.stdin---> decompress ---> content---> compress---> process.stdout---> endon.tar.gz
最后
- 公众号《JavaScript高级程序设计》
- 公众号内回复”vue-router“ 或 ”router“即可收到 VueRouter源码分析的文档。
- 回复”vuex“ 或 ”Vuex“即可收到 Vuex 源码分析的文档。
全文完,如果喜欢。
请点赞和"在看"吧,最好也加个"关注",或者分享到朋友圈。