Потоки: ReadableStream і WritableStream
Потік (Stream) це послідовність даних, яка надходить поступово, а не вся одразу. Звичайний fetch().text() чекає, доки прийде все, і лише потім повертає результат, а потік (response.body) дозволяє обробляти дані частинами, одразу в міру надходження (chunk by chunk).
Теорія
TL;DR
- Потік це дані, що надходять порціями (chunks), а не одним шматком.
ReadableStreamце джерело,WritableStreamце приймач,TransformStreamце перетворювач.response.bodyпісляfetch()це готовийReadableStream.reader.read()повертає проміс із{ done, value }, деvalueцеUint8Array.- Потоки з'єднуються в ланцюжки через
pipeThrough()іpipeTo(). - Головна вигода: обробка великих обсягів без завантаження всього в пам'ять.
Швидкий приклад
const response = await fetch('https://example.com/large-file.txt');
// response.body це ReadableStream
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log('chunk size:', value.length);
}Що відбувається:
fetchповертаєResponse, у якого.bodyце ReadableStream.- Метод
.getReader()створює читача, який віддає дані порціями (chunks). reader.read()повертає проміс із{ done, value }:valueце шматок даних (типUint8Array);done: trueозначає, що потік закінчився.
Так можна завантажувати гігабайти, не тримаючи весь файл в оперативній пам'яті.
Типи потоків у JavaScript
| Тип | Призначення |
|---|---|
| ReadableStream | джерело даних (читання) |
| WritableStream | приймач даних (запис) |
| TransformStream | проміжна обробка (читання плюс запис) |
| DuplexStream | двосторонній, наприклад сокет |
Запис даних у WritableStream
const stream = new WritableStream({
write(chunk) {
console.log('writing chunk:', chunk);
},
close() {
console.log('stream finished');
},
});
const writer = stream.getWriter();
await writer.write('Привіт');
await writer.write('Світ');
await writer.close();Пояснення:
WritableStreamприймає дані частинами черезwrite(chunk);close()викликається, коли потік завершено;- це зручно для відправлення даних, запису файлів, логування тощо.
TransformStream і перетворення на льоту
const upperCaseStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
},
});
const writer = upperCaseStream.writable.getWriter();
const reader = upperCaseStream.readable.getReader();
await writer.write('hello ');
await writer.write('world!');
await writer.close();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // "HELLO " "WORLD!"
}TransformStream поєднує потік на вході (writable) і на виході (readable) та дозволяє перетворювати дані «на льоту».
З'єднання потоків через pipeTo і pipeThrough
Потоки можна з'єднувати між собою, як водопровідні труби:
await readableStream
.pipeThrough(transformStream)
.pipeTo(writableStream);Тобто:
pipeThroughзастосовує перетворення і повертає новий readable;pipeToпередає результат у кінцевий приймач.
Практичний приклад із fetch() і TextDecoderStream:
const response = await fetch('https://example.com/data.txt');
const textStream = response.body
.pipeThrough(new TextDecoderStream()) // декодуємо байти в текст
.pipeThrough(
new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase()); // перетворюємо текст
},
})
);
const reader = textStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value);
}Кожен шматок файлу спочатку перетворюється на текст, потім на верхній регістр, і одразу обробляється, без очікування кінця завантаження.
Потоки в Node.js і навіщо вони потрібні
У Node.js є власна реалізація (fs.createReadStream, stream.Writable та інші), але концепція та сама:
import fs from 'fs';
const readStream = fs.createReadStream('file.txt', { encoding: 'utf-8' });
readStream.on('data', chunk => console.log('Chunk:', chunk));
readStream.on('end', () => console.log('Done'));Потоки Node.js побудовані на EventEmitter, а браузерні потоки на промісах і контролерах. Сучасний Node.js також підтримує Web Streams через node:stream/web, тому код можна переносити між середовищами.
| Перевага | Опис |
|---|---|
| Продуктивність | Не треба чекати завантаження всього файлу |
| Економія пам'яті | Обробка йде «на льоту», порціями |
| Композиція | Потоки можна об'єднувати в ланцюжки |
| Двосторонній зв'язок | Використовуються у WebSocket, fetch(), Response, Body |
| Контроль | Читання можна призупиняти та відновлювати (backpressure) |
Проста аналогія: потоки це конвеєр. Дані не приходять одразу цілком, вони «їдуть» стрічкою порціями (chunks), і на кожному етапі (Readable, Transform, Writable) ти можеш їх обробляти, фільтрувати та зберігати.
| Тип | Призначення | Приклад використання |
|---|---|---|
| ReadableStream | Джерело даних | fetch().body, file.stream() |
| WritableStream | Приймач даних | Запис у файл, лог, мережу |
| TransformStream | Перетворювач | Стиснення, шифрування, фільтрація |
| pipeTo / pipeThrough | З'єднання потоків | readable.pipeThrough(transform).pipeTo(writable) |
Типові помилки
- Читати
response.bodyпісляresponse.text(). Тіло відповіді можна спожити лише один раз: післяtext(),json()абоblob()потік уже заблокований. Якщо потрібні обидва варіанти, скористайтесяresponse.clone(). - Забувати звільняти читача. Поки триває
getReader(), потік залишається заблокованим. Викликайтеreader.releaseLock()або обробляйте потік до кінця. - Ігнорувати backpressure. Якщо писати у
WritableStreamбезawait writer.write(...), черга розростається і перевага потоків над завантаженням у пам'ять зникає. - Вважати, що
valueце рядок. Зresponse.bodyприходитьUint8Array; щоб отримати текст, потрібенTextDecoderStreamабоTextDecoder. - Не обробляти помилки в ланцюжку.
pipeTo()повертає проміс, який може відхилитися; безcatchпомилка мережі залишиться непоміченою. - Плутати потоки Node.js і Web Streams. У них різні API: перші працюють через події
dataтаend, другі через проміси та контролери.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.