全栈前端工程师必会的流,了解下。。。

本文深入解析Node.js中的流概念,包括其类型、模式及如何在实际项目中运用可读流和可写流。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先啥是流呢?

  • 流是一组有序的,有起点和终点的字节数据传输手段
  • 它不关心文件的整体内容,只关注是否从文件中读到了数据,以及读到数据之后的处理
  • 流是一个抽象接口,被 Node 中的很多对象所实现。比如HTTP 服务器request和response对象都是流。
Node.js 中有四种基本的流类型
  • Readable - 可读的流 (例如 fs.createReadStream()).
  • Writable - 可写的流 (例如 fs.createWriteStream()).
  • Duplex - 可读写的流 (例如 net.Socket).
  • Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate()).
流中的数据有两种模式,二进制模式和对象模式
  • 二进制模式, 每个分块都是buffer或者string对象.
  • 对象模式, 流内部处理的是一系列普通对象.

所有使用 Node.js API 创建的流对象都只能操作 strings 和 Buffer对象。但是,通过一些第三方流的实现,你依然能够处理其它类型的 JavaScript 值 (除了 null,它在流处理中有特殊意义)。 这些流被认为是工作在 “对象模式”(object mode)。 在创建流的实例时,可以通过 objectMode 选项使流的实例切换到对象模式。试图将已经存在的流切换到对象模式是不安全的。

下面就简单的介绍下 首先是可读流

可读流事实上工作在下面两种模式之一:flowing 和 paused
  • 在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。
  • 在 paused 模式下,必须显式调用 stream.read() 方法来从流中读取数据片段。
  • 所有初始工作模式为 paused 的 Readable 流,可以通过下面三种途径切换到 flowing 模式:
    • 监听 'data' 事件
    • 调用 stream.resume() 方法
    • 调用 stream.pipe() 方法将数据发送到 Writable
  • 可读流可以通过下面途径切换到 paused 模式:
    • 如果不存在管道目标(pipe destination),可以通过调用 stream.pause() 方法实现。
    • 如果存在管道目标,可以通过取消 'data' 事件监听,并调用 stream.unpipe() 方法移除所有管道目标来实现。

如果 Readable 切换到 flowing 模式,且没有消费者处理流中的数据,这些数据将会丢失。 比如, 调用了 readable.resume() 方法却没有监听 'data' 事件,或是取消了 'data' 事件监听,就有可能出现这种情况。

可读流的三种状态

在任意时刻,任意可读流应确切处于下面三种状态之一:

  • readable._readableState.flowing = null

  • readable._readableState.flowing = false

  • readable._readableState.flowing = true

  • 若 readable._readableState.flowing 为 null,由于不存在数据消费者,可读流将不会产生数据。 在这个状态下,监听 'data' 事件,调用 readable.pipe() 方法,或者调用 readable.resume() 方法, readable._readableState.flowing 的值将会变为 true 。这时,随着数据生成,可读流开始频繁触发事件。

  • 调用 readable.pause() 方法, readable.unpipe() 方法, 或者接收 “背压”(back pressure), 将导致 readable._readableState.flowing 值变为 false。 这将暂停事件流,但 不会 暂停数据生成。 在这种情况下,为 'data' 事件设置监听函数不会导致 readable._readableState.flowing 变为 true。

  • 当 readable._readableState.flowing 值为 false 时, 数据可能堆积到流的内部缓存中。

创建可读流

let rs=fs.createReadStream(path.join(__dirname,'1.txt'),{
    flags:'r',//文件的操作 默认r
    encoding:'utf8',//编码 默认buffer
    autoClose:true,//是否自动关闭  默认true
    start:0,//开始读取的位置 默认0
    end:10,//结束读取的位置(包含)默认读完
    highWaterMark:3,// 一次读取多少字节 默认读是64k 也就是64*1024
    //还有fd 文件描述符 默认null ,mode 文件操作权限,默认为 0o666
})
//这里的第一个参数路径;要说下: 是相对于项目根目录的;跟我们平常的相对路径是不一样的;
//返回值是一个读流对象;

// 监听data事件;获取每一次读到的内容;流切换到流动模式,数据会被尽可能快的读出
rs.on('data',function (data) {
    console.log(data);
    
});
//监听end事件 该事件会在读完数据后被触发
rs.on('end', function () {
    console.log('读取完成');
});
//还有open close error一些事件 看名字就知道干啥的
//暂停和恢复触发data;通过pause()方法和resume()方法
rs.on('data', function (data) {
    rs.pause();
    console.log(data);
});
setTimeout(function () {
    rs.resume();
},2000);

复制代码
可写流
//和读流参数基本差不多 
let ws=fs.createWriteStream('1.txt', {
    flags: 'w', //默认是'w'
    encoding: 'utf8',//默认是utf8
    highWaterMark: 2, // 写入缓存区的默认大小 默认是16k 16*1024
})
let flag=ws.write('aaaa','utf8',()=>{
    console.log('写入成功');
    
})
//参数:1.写入的数据buffer/string  2.编码编码格式chunk为字符串时有用可选, 3.写入成功后的回调
//返回值为布尔值,系统缓存区满时为false,未满时为true
//end方法;不是on('end',cb);在调完end方法后;不能再调用write方法
ws.end(chunk,[encoding],[callback]);
//表明接下来没有数据要被写入 Writable 通过传入可选的 chunk 和 encoding 参数,可以在关闭流之前再写入一段数据 
ws.on('finish', () => {
  console.error('所有的写入已经完成!');
});
//在调用了 ws.end() 方法,且缓冲区数据都已经传给底层系统之后, 'finish' 事件将被触发。

//drain方法  
//当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 'drain' 事件就会被触发
//建议, 一旦 write() 返回 false, 在 'drain' 事件触发前, 不能写入任何数据块

//感受下
let i = 10;

function write() {
    let flag = true;
    while (i && flag) {
        flag = ws.write("1");
        i--;
        console.log(flag);
    }
}
write(); //true false
ws.on('drain', () => { 
    console.log("drain");
    write();
});

//true false  drain  5次



复制代码
pipe 用法
var from = fs.createReadStream('./1.txt');
var to = fs.createWriteStream('./2.txt');
from.pipe(to);
复制代码
原理
var ws = fs.createWriteStream('./2.txt');
var rs = fs.createReadStream('./1.txt');
rs.on('data', function (data) {
    var flag = ws.write(data);
    if (!flag)
        rs.pause();
});
ws.on('drain', function () {
    rs.resume();
});
rs.on('end', function () {
    ws.end();
});
//将数据的滞留量限制到一个可接受的水平,以使得不同速度的来源和目标不会淹没可用内存。
复制代码
简单的实现下可读流
let fs = require('fs');
let EventEmitter = require('events');

class ReadStream extends EventEmitter {//可以on就是发布订阅 
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.fd = options.fd;
        this.flags = options.flags || 'r';
        this.encoding = options.encoding;
        this.start = options.start || 0;
        this.pos = this.start;
        this.end = options.end;
        this.flowing = false;
        this.autoClose = true;
        this.highWaterMark = options.highWaterMark || 64 * 1024;
        this.buffer = Buffer.alloc(this.highWaterMark);
        this.length = 0;
        this.on('newListener', (type, listener) => {
            if (type == 'data') {//检测到有监听data的事件  就变成流动模式
                this.flowing = true;
                this.read();
            }
        });
        this.on('end', () => {
            if (this.autoClose) {
                this.destroy();
            }
        });
        this.open();//只要调用这个类 默认打开文件
    }

    read() {
        if (typeof this.fd != 'number') {
            return this.once('open', () => this.read());
        }
        let n = this.end ? Math.min(this.end - this.pos, this.highWaterMark) : this.highWaterMark;
        
        fs.read(this.fd,this.buffer,0,n,this.pos,(err,bytesRead)=>{
        //参数: 文件描述符  读到哪里(容器) 从哪开始 读到哪个位置 偏移 回调 
            if(err){
             return;
            }
            if(bytesRead){
                let data = this.buffer.slice(0,bytesRead);
                data = this.encoding?data.toString(this.encoding):data;
                this.emit('data',data);
                this.pos += bytesRead;
                if(this.end && this.pos > this.end){
                  return this.emit('end');
                }
                if(this.flowing)
                    this.read();
            }else{
                this.emit('end');
            }
        })
    }

    open() {
    //打开文件 (异步)
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) return this.emit('error', err);
            this.fd = fd;
            this.emit('open', fd);
        })
    }


    end() {
        if (this.autoClose) {
            this.destroy();
        }
    }

    destroy() {
    //关闭文件
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }

}

module.exports = ReadStream;
复制代码
简单的实现下可写流
let fs = require('fs');
let EventEmitter = require('events');
class WriteStream extends  EventEmitter{
    constructor(path, options) {
        super(path, options);
        this.path = path;
        this.fd = options.fd;
        this.flags = options.flags || 'w';
        this.mode = options.mode || 0o666;
        this.encoding = options.encoding;
        this.start = options.start || 0;
        this.pos = this.start;
        this.writing = false;
        this.autoClose = true;
        this.highWaterMark = options.highWaterMark || 16 * 1024;
        this.buffers = [];
        this.length = 0;
        this.open();
    }

    open() {
        fs.open(this.path, this.flags, this.mode, (err, fd) => {
            if (err) return this.emit('error', err);
            this.fd = fd;
            this.emit('open', fd);
        })
    }

    write(chunk, encoding, cb) {
        if (typeof encoding == 'function') {
            cb = encoding;
            encoding = null;
        }

        chunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, this.encoding || 'utf8');
        let len = chunk.length;
        this.length += len;
        let ret = this.length < this.highWaterMark;
        if (this.writing) {
            this.buffers.push({
                chunk,
                encoding,
                cb,
            });
        } else {
            this.writing = true;
            this._write(chunk, encoding,this.clearBuffer.bind(this));
        }
        return ret;
    }

    _write(chunk, encoding, cb) {
        if (typeof this.fd != 'number') {
            return this.once('open', () => this._write(chunk, encoding, cb));
        }
        fs.write(this.fd, chunk, 0, chunk.length, this.pos, (err, written) => {
            if (err) {
                if (this.autoClose) {
                    this.destroy();
                }
                return this.emit('error', err);
            }
            this.length -= written;
            this.pos += written;
            cb && cb();
        });
    }

    clearBuffer() {
        let data = this.buffers.shift();
        if (data) {
            this._write(data.chunk, data.encoding, this.clearBuffer.bind(this))
        } else {
            this.writing = false;
            this.emit('drain');
        }
    }

    end() {
        if (this.autoClose) {
            this.emit('end');
            this.destroy();
        }
    }

    destroy() {
        fs.close(this.fd, () => {
            this.emit('close');
        })
    }

}

module.exports = WriteStream;
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值