
浏览器里的"流"到底是什么?把它想成小区的自来水净化站
一句话说清楚:以前处理大文件、大数据,都得先把"一整桶水"倒进内存这个大缸里,等倒满了才能处理;而"流"(Stream)让数据像自来水一样,边流边处理,压根不用等整缸水都到齐。
打个比方:把这套东西想成小区自来水净化站
小区里的自来水,不是先把一个水库的水全部抽到你家,再让你慢慢处理,而是水源接了一根管子,中间串了好几道处理站——过滤站、消毒站、加压站,水一路流过去,边流边处理,最后从水龙头流出来就是能直接喝的水。
浏览器里的"流处理"就是这个思路:数据像水一样,从"进水口"流入,经过几道"处理站"(这就是 TransformStream),最后流到"出水口"(存文件、传网络、显示在页面上)。整个过程不需要先把全部数据攒在内存里,处理超大文件、实时数据的时候,内存不会被撑爆,用户也不用干等着"缓冲进度条"转半天。
带着这根"水管"的画面,我们把 6 个常见的流技术挨个讲清楚。
处理站本体:TransformStream 就是那道"过滤网"
javascript// 把 CSV 格式的水,净化成 JSON 格式的水
const csvToJson = new TransformStream({
transform(chunk, controller) {
try {
const rows = chunk.split('\n');
const json = rows.map(row => {
const [name, age] = row.split(',');
return { name, age: parseInt(age, 10) };
});
controller.enqueue(json); // 处理完一小段,立刻放行到下一站
} catch (error) {
controller.error(`CSV 解析失败:${error.message}`); // 这一小段水质有问题,报警
}
}
});transform() 这个方法就是过滤网本身——水流过来一小段(一个 chunk),它就地处理一下,处理完立刻 enqueue 放行到下一道工序,不用等"一整桶水"都到齐才开始干活。
业务场景很实在:假设你要把一个几个 GB 的传感器日志文件,从 CSV 转成 JSON 存进数据库。要是老老实实"整桶端进来再处理",浏览器内存分分钟爆掉;换成流处理,数据一边流一边转换,哪怕文件有 10 个 G,浏览器也不会卡死——原理就是自来水站从不会因为水库水多就把水管撑爆,因为水从来都是一小段一小段过去的。
接水管的第一步:Blob 直接变成一根"水管"
javascriptdocument.querySelector('input[type=file]').addEventListener('change', async (e) => {
const blobStream = e.target.files[0].stream(); // 文件秒变一根水管
const processed = blobStream.pipeThrough(new VideoCompressor()); // 接上"压缩处理站"
await processed.pipeTo(uploadToServer()); // 边压缩边传,不用等压完再传
});以前处理用户上传的文件,习惯是"先把整个文件读进内存变成一大坨数据,再开始处理"——这就好比先把一整桶水倒进自己家水缸,再从缸里舀水处理,多此一举。
file.stream() 干的事,就是直接把这个文件"接成一根水管",不用先倒进缸里。业务场景:用户上传一个几百 MB 的视频,你想边上传边压缩边转码,pipeThrough 就是往这根水管上串联处理站,pipeTo 就是接到最终出口(服务器),全程没有"先囤积、再处理"这一步,用户体验自然快很多。
水质检测站:文字编码解码流
javascript// 一个实时翻译流水线:原始二进制 → 西班牙语文字 → 翻译 → 英文文字 → 二进制存盘
fetch('/api/spanish-text')
.then(response =>
response.body
.pipeThrough(new TextDecoderStream()) // 二进制水 → 文字水
.pipeThrough(translateToEnglish) // 翻译处理站(自定义的过滤网)
.pipeThrough(new TextEncoderStream()) // 文字水 → 二进制水,方便存盘
.pipeTo(saveFileStream)
);网络里传的数据本质上都是二进制"原水",人看不懂。TextDecoderStream 就像水质检测站,自动把这些看不懂的原始信号翻译成人类能读的文字;处理完之后如果还要存盘或者再传输,TextEncoderStream 再把文字重新编码回二进制。
业务场景:做一个多语言客服系统,后端不断吐出西班牙语的对话记录,你要一边接收一边实时翻译成中文展示给客服人员看——用这套流水线,数据到一句、翻一句、显示一句,不用等整段对话说完才开始处理,用户体验就是"实时同传"的感觉。
浓缩站:CompressionStream 边流边挤水分
javascriptconst uploadCompressed = async (file) => {
const readable = file.stream();
const compressed = readable.pipeThrough(new CompressionStream('gzip')); // 边流边压缩
await fetch('/upload', {
method: 'POST',
body: compressed,
headers: { 'Content-Encoding': 'gzip' }
});
};把 CompressionStream 想成净水站里的"浓缩工序"——水还在管子里流着的时候,就顺手把多余的水分挤掉,体积变小了再往下游送,到了目的地再还原回来就行。
业务场景很直接:公司 APP 的安装包有 200MB,用户上传或者下发更新包的时候,边传边压缩,实测能把体积压掉 78%——这意味着服务器带宽成本直接砍掉大头,用户下载也更快。以前得先把文件整个压缩好、生成一个新文件,再上传,现在这两步在水管里一次性搞定。
双向水管带阀门:WebSocketStream
javascriptconst wss = new WebSocketStream('wss://stock-prices');
const { readable, writable } = await wss.connection;
// 只放行超过预警线的报价,别把界面刷爆
readable.pipeThrough(new TransformStream({
transform(quote, controller) {
if (quote.price > alertThreshold) controller.enqueue(quote);
}
})).pipeTo(updateDashboard);普通的 WebSocket 用起来,很容易出现"消息堆积"的问题——服务器狂推消息,前端处理不过来,全堆在内存里,界面越来越卡。WebSocketStream 相当于这根水管自带一个智能阀门:下游(界面渲染)处理不过来的时候,阀门会自动调小上游的流速,不会因为你家水龙头开得太小,把整根水管憋炸——这就是"背压"(backpressure)机制。
业务场景:做一个实时股票行情看板,服务器每秒推几十条报价,你只想展示"超过预警线"的那几条。用上面这段代码,处理站(TransformStream)先做一道筛选,只放行需要关注的数据,界面渲染压力小了很多。用完记得调用 releaseLock() 把水管松开,不然这根管子会一直占着资源不释放,时间长了就是内存泄漏。
接到硬件水龙头:SerialPort 流
javascriptconst port = await navigator.serial.requestPort(); // 找到那个硬件"水龙头"
await port.open({ baudRate: 9600 });
// 一滴一滴读传感器的数据
const reader = port.readable.getReader();
while (true) {
const { value } = await reader.read();
console.log('传感器读数:', value);
}这是最容易被忽略的一种"水管"——它接的不是文件也不是网络请求,而是实打实的硬件设备,比如通过 USB 连着的 Arduino 传感器。浏览器直接向硬件"打开水龙头",一滴一滴地把数据读出来。
业务场景:做智能硬件或者物联网相关的项目,比如一个温湿度监测面板,传感器每秒吐一条数据,中间数据难免有噪声干扰。这时候完全可以把 TransformStream 接到这根硬件水管上,边读边过滤掉异常值,展示给用户的永远是干净的数据。
一句话收尾
这 6 种"流"说到底都是同一个思路的不同应用场景:数据不是先囤积起来再处理,而是边流边处理,就像小区自来水从不会等水库存满才供水一样。以后再看到 pipeThrough pipeTo 这些方法,直接脑补成"往水管上再接一道处理站",ReadableStream、WritableStream 这些概念立刻就不抽象了。
如果这篇对你有帮助,欢迎关注公众号「前端达人」,每周更新实用前端干货。

