使用流,我们可以从网络或其他来源接收资源,并在第一个比特到达时立即处理它。
与其等待资源完全下载后再使用它,我们可以立即使用它。
什么是流
我想到的第一个例子是加载一个YouTube视频--你不必在开始观看之前完全加载它。
或者流媒体直播,你甚至不知道内容何时结束。
内容甚至不需要结束。它可以无限期地生成。
流媒体API
流媒体API使我们能够处理这类内容。
我们有2种不同的流模式:从流中读取,以及向流中写入。
可读流在所有现代浏览器中都可用,除了IE浏览器。
可写流在Firefox和Internet Explorer上不可用。
像往常一样,请查看caniuse.com,了解有关这一问题的最新信息。
我们先来看看可读流
可读流
说到可读流,我们有3类对象。
ReadableStreamReadableStreamDefaultReaderReadableStreamDefaultController
我们可以使用ReadableStream对象来消费流。
这里是可读流的第一个例子。Fetch API允许从网络上获得一个资源,并将其作为一个流来使用。
const stream = fetch('/resource')
.then(response => response.body)
fetch响应的body 属性是一个ReadableStream 对象实例。这就是我们的可读流。
读者
在一个ReadableStream 对象上调用getReader() ,会返回一个ReadableStreamDefaultReader 对象,即阅读器。我们可以这样得到它。
const reader = fetch('/resource').then(response => response.body.getReader())
我们以块为单位读取数据,其中一个块是一个字节或一个类型的数组。块在流中被排队,我们一次读一个块。
一个流可以包含不同种类的块。
一旦我们有了一个ReadableStreamDefaultReader 对象,我们就可以使用read() 方法来访问数据。
一旦创建了一个阅读器,这个流就被锁定了,没有其他的阅读器可以从它那里获得块,直到我们对它调用releaseLock() 。
你可以通过tee一个流来实现这个效果,后面会有更多的介绍
从一个可读流中读取数据
一旦我们有了一个ReadableStreamDefaultReader 对象实例,我们就可以从它那里读取数据。
这就是你如何从flaviocopes.com网页的HTML内容流中,逐个字节地读取第一块内容(由于CORS的原因,你可以在该网页上打开的DevTools窗口中执行)。
fetch('https://flaviocopes.com/')
.then(response => {
response.body
.getReader()
.read()
.then(({value, done}) => {
console.log(value)
})
})

如果你打开每个单组的数组项目,你会得到单项。那些是字节,存储在一个Uint8Array 。

你可以使用Encoding API将这些字节转换为字符。
const decoder = new TextDecoder('utf-8')
fetch('https://flaviocopes.com/')
.then(response => {
response.body
.getReader()
.read()
.then(({value, done}) => {
console.log(decoder.decode(value))
})
})
这将打印出页面中加载的字符。

这个新版本的代码加载了流中的每一个块,并打印了它。
(async () => {
const fetchedResource = await fetch('https://flaviocopes.com/')
const reader = await fetchedResource.body.getReader()
let charsReceived = 0
let result = ''
reader.read().then(function processText({ done, value }) {
if (done) {
console.log('Stream finished. Content received:')
console.log(result)
return
}
console.log(`Received ${result.length} chars so far!`)
result += value
return reader.read().then(processText)
})
})()
我将其包装在一个async 立即调用的函数中,以使用await 。
我们创建的processText()函数接收一个有两个属性的对象。
done如果流结束,我们得到了所有的数据,则为truevalue收到的当前块的值
我们创建这个递归函数来处理整个流。
创建一个流
警告:Edge和Internet Explorer不支持
我们刚刚看到了如何消费一个由Fetch API生成的可读流,这是一个开始使用流的好方法,因为用例很实用。
现在让我们看看如何创建一个可读流,这样我们就可以用我们的代码来访问一个资源。
我们之前已经使用了一个ReadableStream 对象。现在让我们使用new 关键字创建一个全新的对象。
const stream = new ReadableStream()
现在这个流不是很有用。它是一个空的流,如果有人想从它那里读取数据,就没有数据。
我们可以在初始化过程中通过一个对象来定义流的行为方式。这个对象可以定义这些属性。
start一个在创建可读流时调用的函数。在这里你连接到数据源并执行管理任务。pull在没有达到内部队列高水位的情况下,反复调用一个函数来获取数据cancel当流被取消时调用的函数,例如在接收端调用 方法时。cancel()
下面是一个对象结构的赤裸裸的例子。
const stream = new ReadableStream({
start(controller) {
},
pull(controller) {
},
cancel(reason) {
}
})
start() 和 得到一个控制器对象,是 对象的一个实例,它让你控制流的状态和内部队列。pull() ReadableStreamDefaultController
为了将数据添加到流中,我们调用controller.enqueue() ,并传递持有我们数据的变量。
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hello')
}
})
当我们准备关闭流时,我们调用controller.close() 。
cancel() 得到一个 ,这是取消流时提供给 方法调用的一个字符串。reason ReadableStream.cancel()
我们还可以传递一个可选的第二个对象,它决定了排队策略。它包含2个属性。
highWaterMark可以存储在内部队列中的块的总数量。我们在之前谈论 的时候提到了这个pull()size,一个你可以用来改变块大小的方法,用字节表示
{
highWaterMark,
size()
}
这些主要用于控制流的压力,特别是在管道链的背景下,这在Web APIs中仍然是试验性的。
当一个流的highWaterMark ,一个背压信号被发送到管道中的前几个流,告诉它们放慢数据压力。
我们有2个内置对象来定义排队策略。
ByteLengthQueuingStrategy等待,直到块的累计大小(字节)超过指定的高水位线CountQueuingStrategy等待,直到累积的数据块数量超过指定的高水位线
设置32字节的高水位线的例子。
new ByteLengthQueuingStrategy({ highWaterMark: 32 * 1024 }
设置1块高水位线的例子。
new CountQueuingStrategy({ highWaterMark: 1 })
我提到这一点是为了告诉你,你可以控制流入流的数据量,并与其他角色进行通信,但我们不打算讨论更多细节,因为事情很快就会变得复杂。
发放流
之前我提到,一旦我们开始读取一个流,它就被锁定了,其他的读者不能访问它,直到我们对它调用releaseLock() 。
然而,我们可以使用流本身的tee() 方法来复制该流。
const stream = //...
const tees = stream.tee()
tees 现在是一个包含2个新流的数组,你可以使用 和 来读取这些流。tees[0] tees[1]
可写流
当涉及到可写流时,我们有3类对象。
WritableStreamWritableStreamDefaultReaderWritableStreamDefaultController
我们可以使用WritableStream对象来创建我们以后可以消费的流。
这就是我们如何创建一个新的可写流。
const stream = new WritableStream()
我们必须传递一个对象,以便发挥作用。这个对象将有以下可选的方法实现。
start()当对象被初始化时被调用write()当一个块准备好被写入水槽时被调用(在写入前保存流数据的底层结构)。close()当我们完成写块时被调用abort()当我们想发出错误信号时被调用
下面是一个骨架。
const stream = new WritableStream({
start(controller) {
},
write(chunk, controller) {
},
close(controller) {
},
abort(reason) {
}
})
start(),close() 和write() 被传递给控制器,一个WritableStreamDefaultController 对象实例。
至于ReadableStream() ,我们可以传递第二个对象给new WritableStream() ,以设置排队策略。
例如,让我们创建一个流,给定一个存储在内存中的字符串,创建一个消费者可以连接到的流。
我们首先定义一个解码器,我们将使用Encoding API TextDecoder() 构造函数将我们收到的字节转化为字符。
const decoder = new TextDecoder("utf-8")
我们可以初始化实现close() 方法的WritableStream,当消息被完全接收并且客户端代码调用它时,它将打印到控制台。
const writableStream = new WritableStream({
write(chunk) {
//...
},
close() {
console.log(`The message is ${result}`)
}
})
我们通过初始化一个ArrayBuffer并将其添加到大块中来开始实现write() 。然后,我们使用Encoding API的decoder.decode()方法将这个作为字节的块解码成一个字符。然后我们把这个值添加到一个result ,我们在这个对象外面声明这个字符串。
let result
const writableStream = new WritableStream({
write(chunk) {
const buffer = new ArrayBuffer(2)
const view = new Uint16Array(buffer)
view[0] = chunk
const decoded = decoder.decode(view, { stream: true })
result += decoded
},
close() {
//...
}
})
现在WritableStream对象已经被初始化。
我们现在去实现将使用这个流的客户端代码。
我们首先从writableStream 对象中获取WritableStreamDefaultWriter 对象。
const writer = writableStream.getWriter()
接下来我们定义一个要发送的消息。
然后,我们初始化编码器,对我们要发送到流中的字符进行编码。
const encoder = new TextEncoder()
const encoded = encoder.encode(message, { stream: true })
在这一点上,字符串已经被编码为一个字节数组。现在,我们在这个数组上使用forEach 循环,将每个字节发送到流中。在每次调用流写入器的write() 方法之前,我们检查ready 属性,该属性返回一个承诺,所以我们只在流写入器准备好时写入。
encoded.forEach(chunk => {
writer.ready.then(() => {
return writer.write(chunk)
})
})
现在我们唯一错过的是关闭写入器。forEach 是一个同步循环,这意味着我们只有在每个项目被写入后才能达到这一点。
我们仍然检查ready 属性,然后我们调用close()方法。
writer.ready.then(() => {
writer.close()
})