面试高级前端工程师必问之流-stream

流(stream)是一种在 Node.js 中处理流式数据的抽象接口。 stream 模块提供了一些基础的 API,用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,发送到 HTTP 服务器的请求和 process.stdout 都是流的实例。

流可以是可读的、可写的、或是可读写的。 所有的流都是 EventEmitter 的实例。

流的类型

Node.js 中有四种基本的流类型(本篇主要说前两种):

  • Writable - 可写入数据的流(例如 fs.createWriteStream())
  • Readable - 可读取数据的流(例如 fs.createReadStream())
  • Duplex - 可读又可写的流(例如 net.Socket)
  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())

缓冲

流中一个相当重要的概念,无论读写流都是通过缓冲来实现的。 可写流和可读流都会在一个内部的缓冲器中存储数据,可以分别使用的 writable.writableBuffer 或 readable.readableBuffer 来获取,可缓冲的数据的数量取决于传入流构造函数的 highWaterMark 选项,默认情况下highWaterMark 64*1024个字节 读写的过程都是将数据读取写入缓冲,然后在将数据读出或者写入文件。

几个重要的底层方法

  1. writable.write(chunk[, encoding][, callback]) writable.write() 方法向流中写入数据,并在数据处理完成后调用 callback 。如果有错误发生, callback 不一定 以这个错误作为第一个参数并被调用。要确保可靠地检测到写入错误,应该监听 'error' 事件。 在确认了 chunk 后,如果内部缓冲区的大小小于创建流时设定的 highWaterMark 阈值,函数将返回 true 。 如果返回值为 false ,应该停止向流中写入数据,直到 'drain' 事件被触发。 当一个流不处在 drain 的状态, 对 write() 的调用会缓存数据块, 并且返回 false。 一旦所有当前所有缓存的数据块都排空了(被操作系统接受来进行输出), 那么 'drain' 事件就会被触发。
  2. readable.read([size])

来一个小例子,有助于理解

// pipe
let fs = require('fs');
let rs = fs.createReadStream('./1.txt',{
    highWaterMark:1
})
let ws = fs.createWriteStream('./5.txt',{
    highWaterMark:2
})
let index = 1;
rs.on('data', (data) => {
    console.log(index++)
    let flag = ws.write(data);    // 当内部的可写缓冲的总大小小于 highWaterMark 设置的阈值时,
    //调用 writable.write() 会返回 true。 一旦内部缓冲的大小达到或超过 highWaterMark 时,则会返回 falseif (!flag) {     //内部缓冲超过highWaterMark
        rs.pause()
    }
})
let wsIndex = 1;
ws.on('drain', () => {
    console.log('ws'+wsIndex++)
    rs.resume()
})
// 1 2 ws1 3 4 ws2 5 6 ws3
复制代码

几个重要的事件监听

前面已经说了所有的流都是 EventEmitter 的实例,那么就可以on,可以emit等等

  1. rs.on('data',()) //读入缓冲
  2. ws.on('drain',()) //写的缓冲被清空
    上面的例子中 当写缓冲大于highWaterMark时 我们就要暂停读取,等待监听到drain事件,然后重新启动rs.resume()读取

其实啊,在工作中也是很少直接这用到的,我们可以直接用pipe rs.pipe(ws)即可 这样就给一个可读流写入到一个可写流当中

自己实现的可读流

let EventEmitter = require('events');   //所有的流都是 EventEmitter 的实例,流继承EventEmitter
let fs = require('fs');
class ReadStream extends EventEmitter {
  constructor(path, options = {}) {
    super();
    this.path = path;
    this.autoClose = options.autoClose || true;
    this.flags = options.flags || 'r';
    this.encoding = options.encoding || null;
    this.start = options.start || 0;
    this.end = options.end || null;
    this.highWaterMark = options.highWaterMark || 64 * 1024;
    // 应该有一个读取文件的位置 可变的(可变的位置)
    this.pos = this.start;
    // 控制当前是否是流动模式
    this.flowing = null;
    // 构建读取到的内容的buffer
    this.buffer = Buffer.alloc(this.highWaterMark);
    // 当创建可读流 要将文件打开
    this.open(); // 异步执行
    this.on('newListener', (type) => {
      if(type === 'data'){ // 用户监听了data事件,就开始读取吧
        this.flowing = true;
        this.read();// 开始读取文件
      }
    });
  }
  read(){
    // 这时候文件还没有打开呢,等待着文件打开后再去读取
    if(typeof this.fd !== 'number'){
      // 等待着文件打开,再次调用read方法
      return this.once('open',()=>this.read());
    }
    // 开始读取了
    // 文件可能有10个字符串
    // start 0 end 4
    // 每次读三个 3
    // 0-2
    // 34
    let howMuchToRead = this.end ? Math.min(this.highWaterMark,this.end - this.pos+1) :this.highWaterMark
    // 文件描述符 读到哪个buffer里 读取到buffer的哪个位置
    // 往buffer里读取几个,读取的位置
    fs.read(this.fd, this.buffer,0,howMuchToRead,this.pos,(err,bytesRead)=>{
      if (bytesRead>0){ // 读到内容了
        this.pos += bytesRead;
        // 保留有用的
        let r = this.buffer.slice(0, bytesRead);
        r = this.encoding ? r.toString(this.encoding) : r;
        // 第一次读取
        this.emit('data', r);
        if (this.flowing) {
          this.read();
        }
      }else{
        this.end = true;
        this.emit('end');
        this.destroy();
      }
    });
  }
  destroy() { // 判断文件是否打开 (将文件关闭掉)
    if (typeof this.fd === 'number') {
      fs.close(this.fd, () => {
        this.emit('close');
      });
      return;
    }
    this.emit('close');
  }
  open() { // 打开文件的逻辑
    fs.open(this.path, this.flags, (err, fd) => {
      if (err) {
        this.emit('error', err);
        if (this.autoClose) {
          this.destroy(); // 销毁 关闭文件(触发close事件)
        } return;
      }
      this.fd = fd;
      this.emit('open'); // 触发文件开启事件
    });
  }
  pause(){
    this.flowing = false;
  }
  resume(){
    this.flowing = true;
    this.read(); // 继续读取
  }
}
module.exports = ReadStream;
复制代码

自己实现的可写流

let fs = require('fs');
let EventEmitter = require('events');

class WriteStream extends EventEmitter{
  constructor(path,options ={}){
    super();
    this.path = path;
    this.flags = options.flags || 'w';
    this.mode = options.mode || 0o666;
    this.highWaterMark = options.highWaterMark || 16*1024;
    this.start = options.start || 0;
    this.autoClose = options.autoClose|| true;
    this.encoding = options.encoding || 'utf8';

    // 是否需要触发drain事件
    this.needDrain = false;
    // 是否正在写入
    this.writing = false;
    // 缓存 正在写入就放到缓存中
    this.buffer = [];
    // 算一个当前缓存的个数
    this.len = 0;
    // 写入的时候也有位置关系
    this.pos = this.start;
    this.open();
  }
  // 0 [1 2] 
  write(chunk, encoding = this.encoding,callback){
    chunk = Buffer.isBuffer(chunk)?chunk:Buffer.from(chunk);
    this.len += chunk.length;// 每次调用write就统计一下长度
    this.needDrain = this.highWaterMark <= this.len; 
    // this.fd
    if(this.writing){
      this.buffer.push({chunk,encoding,callback});
    }else{
      // 当文件写入后 清空缓存区的内容
      this.writing = true;  // 走缓存
      this._write(chunk,encoding,()=>this.clearBuffer());
    }
    return !this.needDrain; // write 的返回值必须是true / false   
    
    //这时候可以回头看一下上面的例子,在this.len >= this.higWaterMark的时候,返回了一个fasle,例子中就暂停读取了。等待写入完成
  }
  _write(chunk,encoding,callback){
    if (typeof this.fd !== 'number') {
      return this.once('open', () => this._write(chunk, encoding, callback));
    }
    // fd是文件描述符 chunk是数据 0 写入的位置和 长度 , this.pos偏移量
    fs.write(this.fd, chunk,0,chunk.length,this.pos,(err,bytesWritten)=>{
      this.pos += bytesWritten;
      this.len -= bytesWritten; // 写入的长度会减少
      callback();
    });
  }
  clearBuffer(){
    let buf = this.buffer.shift();
    if(buf){
      this._write(buf.chunk, buf.encoding, () => this.clearBuffer());
    }else{
      this.writing = false;
      this.needDrain = false; // 触发一次drain  再置回false 方便下次继续判断
      this.emit('drain');
    }
  }
  destroy(){
    if(typeof this.fd === 'number'){
      fs.close(this.fd,()=>{
        this.emit('close');
      });
      return 
    }
    this.emit('close');
  }
  open(){
    fs.open(this.path,this.flags,this.mode,(err,fd)=>{
      if(err){
        this.emit('error');
        this.destroy();
        return 
      }
      this.fd = fd;
      this.emit('open');
    });
  }
}
module.exports = WriteStream;
复制代码

以上就是流的一些基础知识,流的简单应用以及自己实现的可读流可写流。当然有很多不足之处,希望朋友们提出指正。也希望和各位朋友一起学习分享!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
application/octet-stream是一种通用的二进制数据流类型,常用于文件下载。在前端中,可以通过接口返回的type字段来判断是否是application/octet-stream类型的数据,从而进行相应的处理。例如,可以使用if语句来判断type字段的值,然后执行相应的逻辑。比如,如果type为"application/json",可以进行JSON数据的解析和处理;如果type为"application/octet-stream",可以进行文件下载的操作。在Vue中,可以利用作用域插槽获取每行数据对应的文件名称,然后将数据转换为Blob对象,创建下载链接,并设置下载链接的文件名,最后通过模拟点击下载链接来实现文件下载。 #### 引用[.reference_title] - *1* *2* [前端接收 type: “application/octet-stream“ 格式的数据并下载,解决后端返回不唯一](https://blog.csdn.net/qq_53145332/article/details/123595850)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [vue中后端返回文件流( type “applicationoctet-stream“ )的形式,前端进行处理和文件下载,以及自定...](https://blog.csdn.net/m0_67392182/article/details/123304445)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值