<一>、 概念
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)
})
可写流
:将流数据写入目的地(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>
-
-
转换流
: 表示链式传输管道,可写流用于接收数据(可写端),可读流用于输出数据(可读端),可读流和可写流之间的转换程序,可以根据需要检查和修改流内容, 可以用于解码/编码视频帧,解压数据或者将流从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) && chunk.every(val => typeof val === 'number')) {
controller.enqueue(new Uint8Array(chunk))
} else if('function' === typeof chunk.valueOf && 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()})
}
}