Stream API 解析

60 阅读4分钟

<一>、 概念

1. Stream Api 解决了什么问题?

  • 曾经,如果我们想要处理某种资源(视频、文本文件等),我们必须要下载完整的文件,然后等待它反序列化成适当的格式,然后在完整地接受到所有的内容后再进行处理
  • 使用流,只要原始数据在客户端可用,就可以通过js按位处理数据,不再需要缓冲区、字符串或者blob
  • 流会将我们通过网络请求获取的资源分成一个个小的块,让后按位处理这些数据

2. 主要的应用场景?
大块的数据可能不会一次性都使用。网络请求响应是一个典型的例子。网络负载是以连续信息包的形式交付的,而流式处理可让数据一到达就能使用,而不必等待所有数据都加载完毕
大块数据可能要分为小部分处理。视频处理、数据压缩、图像编码、JSON解析都是可以分成小部分进行处理的,而不必等到素有数据都在内存中时再处理

3. 理解流

  • 流的基本单位是。块可以是任意数据类型,通常是一个定型数组
  • 每个块都是一个离散的流片段,可以作为一个整体进行处理
  • 块不是以固定大小的流片段,也不会按照固定的间隔到达指定的端(理想流当中的块的大小近似相等,到达的间隔时间也近似相同)

4. 流平衡的三种情形

  • 流出口处理数据的速度比流入口处理数据的快,流入口经常处于空闲状态,这样会浪费一点内存和计算资源,可接受
  • 流入和流出均衡,理想状态
  • 流入口数据处理速度比流出口数据快,流不平衡

5. 解决流不平衡的问题
针对流不平衡的问题,所有的流都会为已入流但未离开流的块提供一个内部队列
如果块入列速度大于块出列速度,内部队列就会不断的增大。流不能允许内部队列无限扩增大,会使用反压通知流入口停止发送数据,知道队列大小降到某个既定的阈值之下,这个值由排列策略决定,这个策略决定了内部队列可以占用的最大内存(高水位线)

<二>、Stream API

Stream API 定义了三种流:

1.可读流:可以通过某个公共接口读取数据块的流。数据在内部从底层源进入流,然后由消费者(consumer)进行处理

  • ReadableStream: 表示数据的可读流。用于处理fetch API 返回的响应,或者开发者自定义的流
  • ReadableStreamDefaultReader: 表示默认reader,用于读取来自网络的数据流,读取器对象
  • ReadableStreamDefaultController: 表示一个controller, 用于控制ReadableStream 的状态及内部队列。默认的controller用于处理非字节流
// 配合Fetch api使用,处理从网络获取的资源
fetch("http://localhost:9999")
.then(response => response.body)
.then(rb => {
	// 创建一个读取器对象,并锁定流
	const reader = rb.getReader()
	// 读取并处理读取器对象中的流片段
	return new ReadableStream({
		start(controller){
			// controller 控制器对象,用于控制ReadableStream内部状态和队列
			// 读取读取器中锁定的流信息
			function push() {
				reader.read().then(({done, value}) => {
					if(done) {
						// 流处理完成
						console.log('process done:', done)
						// 关闭控制器
						controller.close()
						return
					}
					// 将流添加到内部队列中
					controller.enqueue(value)
					console.log('processing stream:', done, value)
					// 递归处理流
					push()
				})
			}
			push()
		}
	})
})
.then(stream => {
	console.log('获取处理好的流信息:', stream)
	return new Response(stream, {header: {"Content-Type": "application/json"}}).text()
})
.then(res => {
	console.log("获取转换后的结果:", res)
}) 
  1. 可写流:将流数据写入目的地(sink)提供的一个标准抽象,是一个可转移的对象。生产者(producer)将数据写入,数据在内部传入底层数据槽
  • WritableStream: 提供将流写入目标整个过程的标准抽象表示(sink),内置被压和队列机制
  • WritableStreamDefaultWriter: 表示writer, 用于将数据写入可写流中
  • WritableStreamDefaultController: controller, 用于控制WritableStream的状态
    • <!DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>可写流</title>
      </head>
      
      <body>
          <a href="#" onclick="handelWrite()">开始写入流</a>
          <div id="box"></div>
          <script>
              const box = document.querySelector('#box')
              function sendMessage(message, writableStream) {
                  // 获取 WritableStreamDefaultWrite 实例,用于将数据写入可写流中
                  const defaultWrite = writableStream.getWriter();
                  const encoder = new TextEncoder();
                  // 将message内容进行编码
                  const encoded = encoder.encode(message, { stream: true });
                  encoded.forEach((chunk) => {
                      defaultWrite.ready
                          .then(() => {
                              // 写入流
                              if(chunk) {
                                  console.log('开始写入流:', chunk);
                                  return defaultWrite.write(chunk)
                              }
                          })
                          .catch((err) => {
                              throw (err)
                          })
                  })
                  defaultWrite.ready
                      .then(() => {
                          defaultWrite.close()
                      })
                      .catch(err => {
                          console.log('Stream error:', err)
                      })
              }
      
      
              const decoder = new TextDecoder("utf-8")
              const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1 })
              let result = ''
              const writableStream = new WritableStream({
                  write(chunk) {
                      return new Promise((resolve, reject) => {
                          let buffer = new ArrayBuffer(1)
                          let view = new Uint8Array(buffer)
                          view[0] = chunk
                          let decoded = decoder.decode(view, { stream: true })
                          const listItem = document.createElement('p')
                          listItem.textContent = "chunk decoded:" + decoded
                          box.appendChild(listItem)
                          result += decoded
                          resolve()
                      })
                  },
                  close() {
                      let listItem = document.createElement('p')
                      listItem.textContent = "[MESSAGE RECIVED]" + result
                      box.appendChild(listItem)
                  },
                  abort(error) {
                      console.log("Sink error:", error);
                  }
              }, queuingStrategy)
      
      
              function handelWrite() {
                  sendMessage('Hello World', writableStream)
              }
          </script>
      </body>
      
      </html>
      
  1. 转换流: 表示链式传输管道,可写流用于接收数据(可写端),可读流用于输出数据(可读端),可读流和可写流之间的转换程序,可以根据需要检查和修改流内容, 可以用于解码/编码视频帧,解压数据或者将流从XML转换到JSON

  • TransformStream: 表示一组可转化的数据
  • TransformStreamDefaultController: 提供操作和转换流关联的ReadableStream 和 WritableStream 的方法
// 将任意对象转化为unit8数组
const transformContent = {
          start() {}, // 必传项
          async transform(chunk, controller) {
                 chunk = await chunk
                 switch(typeof chunk) {
                    case 'object':
                        if(chunk === null) {
                            controller.terminate()
                        } else if (ArrayBuffer.isView(chunk)) {
                            controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))
                        } else if (Array.isArray(chunk) &amp;&amp; chunk.every(val => typeof val === 'number')) {
                            controller.enqueue(new Uint8Array(chunk))
                        } else if('function' === typeof chunk.valueOf &amp;&amp; chunk.valueOf() !== chunk) {
                            this.transform(chunk.valueOf(), controller)
                        } else if('toJSON' in chunk) {
                            this.transform(JSON.stringify(chunk), controller)
                        }
                        break
                    case 'symbol':
                        console.error(`cannot send a symbol as chunk part`)
                        break
                    case 'undefined':
                        console.error('cannot send a undefined as chunk part')
                    default: 
                        controller.enqueue(this.textencoder.encode(String(chunk)))

                }
            },
            flush() {},
   }
   class AnyTypeToU8Stream extends TransformStream {
       constructor() {
           super({...transformContent, textencoder: new TextDecoder()})
       }
   }