老规矩,先讲大道理。
字节流,字符流,对象流
流就是流动的数据,一切数据传输都是流,无论在平台内部还是平台之间。但有时候我们需要将一个整体数据拆分成若干小块(chunk),在流动的时候对每一小块进行处理,就需要使用流api了。
比如流媒体技术。从浏览器的视角,我们看在线视频,无需等待视频完全缓冲完毕就可以一边观看一边下载。
比如下载大文件。从服务器的视角,从数据库中读一个大文件传给前端,无需先把文件整个儿拿出来放到内存中再传给前端,可以搭一个管道,让文件一点一点流向前端,省时又省力。
那chunk就是流的最小分割单元,按照chunk的大小可以将流分类为字节流,字符流,对象流。这是3种最常用的流,顾名思义,它们的最小分割单元分别是一个字节,一个字符,一个(JS)对象。但是我们今天来手写一个新的流类型:段落流。
在计算机世界中,一行就是一个段落,一个段落就是一行,一个段落chunk就是一个不包含换行符的字符串。以一行为一个chunk的流称为段落流或者叫line流。
科普:在文本中拖拽有3种行为:直接按住拖拽是以单个字符为单位选中文本;双击并按住拖拽会以单词为单位进行选择;单机三次并按住拖拽会议一行为单位进行选择。这是上个世纪就定义好的鼠标行为,但许多人还不知道。
readline源码分析
由于一行的长短不一,许多平台没有提供段落流,幸运的是,nodejs提供了。nodejs标准库内置的readline模块就是一个可以从可读流中逐行读取的接口。
从内存中逐行读取和从外存逐行读取截然不同,因为内存属于计算机,而外存属于外部设备,从计算机核心的角度,从外存读取一个文件和从网络上读取一个文件是一样的。如果单纯从内存中读取一行字符串非常容易,但从外存,从文件系统中读取一行就要考虑时空效率了。
可读流,变形流,可写流
按照流的方向来分类,又出现了3个概念:可读流,变形流,咳血流。按照顺序,数据一般从可读流开始读出,中间经过0个或若干个变形流,最后写入可写流。readline就是一种变形流(transform stream),对写入的字符流变形,组装成段落流并读出。组装的过程可以用下图来解释:
首先我们准备一个缓冲区队列queue(从右向左进入)用来临时存放字符串。由于字符流每次给到我们都是一个字符串chunk,其中可能有若干个换行符\n,我们需要对chunk.split('\n'),然后得到了一个string列表。列表除了最后一个string外,其他string都是确定的字符串,可以按顺序读出,但是最后一个string有可能没结束,有可能下一个trunk进来后又增加了这个string的长度,所以最后一个string暂时留在queue中。
下一个trunk进来后还按照相同的方法把前面的所有string读出,保留最后一个string。所有trunk都按照这个方法操作,直到最后一个trunk结束后,把queue中所有的string都读出。
通过这种算法,段落流每次都能从外存文件中读取一行,最重要的是,消耗的内存完全不受文件大小的影响。
readline算法好像非常简单,不如我们手写一个lineReader.js吧:
const Transform = require('stream').Transform; module.exports = class extends Transform { constructor() { super({ // 写入方向 writableObjectMode: false, // 读出方向 readableObjectMode: true, }) this.queue = '' } _transform(chunk, encoding, next) { this.queue += chunk.toString(); const lines = this.queue.split('\n') this.queue = lines.pop(); lines.forEach(line => this.push(line)); // this.push或next二选一传递chunk next(); } // 最后一个chunk结束后 _flush(callback) { this.queue.split('\n').forEach(line => { this.push(line) }) callback(); }
}
看到没,整个lineReader继承了Transform类,覆盖_transform方法来处理每次写入的trunk,覆盖_flush来处理最后一个trunk。整个过程非常简单,使用方法就和其他变形流一样,通过pipe或者监听data事件来流动:
const fs = require('fs');
const lineReader = require('./lineReader.js')
fs.createReadStream('path/to/textFile.txt', { encoding: 'utf8' }) .pipe(new lineReader()) .on('data', line => { console.log('------new line------ ', line);
})
nodejs的readline模块和我们的lineReader原理是一样的,只不过多了一些错误处理机制,封装了一些辅助方法,所以生产环境下还是使用readline模块比较好,毕竟人家是标准库嘛。
标记语言流、函数式代码流
前面提到的流媒体技术不仅服务于图片和音视频,还作用于网页,没想到吧。我们的html和json等标记语言都是可以实时渲染的(json流化请参考ndjson)。除此之外,函数式编程语言源文件也是可以硫化的,因为函数式编程语言由表达式组成,理论上,一个js文件可以通过“表达式流”来即时编译,可是该死的“变量提升”等机制破坏了JavaScript流化的能力,使得浏览器不得不等待整个js文件传输完成之后才能开始解析。
是个前端都知道,现代的网页中js文件的体积远远大于html文件,这种环境下光html能够即时渲染有什么意义呢?为了生成长html,后端又不得不去使用模板引擎:这又间接破坏了前后端分离。因此,EcmaScript委员会一直呼吁大家使用let替代var,甚至劝大家不要把所有代码放到一个闭包中(使得表达式过大,难以流化)。可是有啥用呢?这么多年过去了一点变化都没有,只能怪假程序员太多,关注代码性能的人太少。
(完)
【每日一猫】
参考资料
https://jimmy.blog.csdn.net/article/details/103221076
https://jimmy.blog.csdn.net/article/details/90678160
https://jimmy.blog.csdn.net/article/details/100915601
https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
https://nodejs.org/api/readline.html#readline_readline