在流中实现readline算法

老规矩,先讲大道理。

字节流,字符流,对象流

流就是流动的数据,一切数据传输都是流,无论在平台内部还是平台之间。但有时候我们需要将一个整体数据拆分成若干小块(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,甚至劝大家不要把所有代码放到一个闭包中(使得表达式过大,难以流化)。可是有啥用呢?这么多年过去了一点变化都没有,只能怪假程序员太多,关注代码性能的人太少。

(完)

【每日一猫】


参考资料

  1. https://jimmy.blog.csdn.net/article/details/103221076

  2. https://jimmy.blog.csdn.net/article/details/90678160

  3. https://jimmy.blog.csdn.net/article/details/100915601

  4. https://developer.mozilla.org/en-US/docs/Web/API/Streams_API

  5. https://nodejs.org/api/readline.html#readline_readline

猜你喜欢

转载自blog.csdn.net/github_38885296/article/details/103296266