Як працюють streams (ReadableStream, WritableStream)?
Що таке потік (Stream)
Потік (Stream) - це послідовність даних, яка надходить поступово, а не вся одразу.
Головна відмінність від звичайних даних:
- звичайний
fetch().text()чекає, поки все прийде → потім повертає результат; - потік (
fetch().body) дозволяє обробляти дані по шматочках, одразу в міру надходження (chunk by chunk).
Типи потоків у JavaScript
| Тип | Призначення |
|---|---|
| ReadableStream | джерело даних (читання) |
| WritableStream | приймач даних (запис) |
| TransformStream | проміжна обробка (читання + запис) |
| DuplexStream | двосторонній (наприклад, сокет) |
Приклад: читання даних з потоку (ReadableStream)
Уяви, ти робиш запит на великий файл:
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 розміром:', value.length);
}Що відбувається:
fetchповертаєResponse, у якого.body- це ReadableStream.- Метод
.getReader()створює читача, який повертає дані порціями (chunks). reader.read()повертає проміс з{ done, value }.
value- шматочок даних (типUint8Array),done: true- потік закінчився.
Так можна завантажувати гігабайти, не завантажуючи все в оперативну пам'ять.
Приклад: запис даних у WritableStream
const stream = new WritableStream({
write(chunk) {
console.log('Записуємо chunk:', chunk);
},
close() {
console.log('Потік завершено');
},
});
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
JS-потоки можна з'єднувати (pipe) як водопровідні труби
await readableStream
.pipeThrough(transformStream)
.pipeTo(writableStream);Таким чином:
pipeThroughзастосовує перетворення;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, а браузерні потоки - на промісах і контролерах.
Чому потоки важливі
| Перевага | Опис |
|---|---|
| Продуктивність | Немає потреби чекати завантаження всього файлу |
| Економія пам'яті | Обробка йде "на льоту", шматками |
| Композиція | Можна об'єднувати потоки в ланцюжки |
| Двосторонній зв'язок | Використовуються у WebSocket, fetch(), Response, Body |
| Контроль | Можна призупиняти/відновлювати читання |
Проста аналогія
Потоки - це як конвеєр
Дані не приходять одразу цілком - вони "їдуть" по стрічці порціями (chunks), і на кожному етапі (Readable → Transform → Writable) ти можеш обробляти, фільтрувати і зберігати їх.
Підсумок
| Тип | Призначення | Приклад використання |
|---|---|---|
| ReadableStream | Джерело даних | fetch().body, file.stream() |
| WritableStream | Приймач даних | Запис у файл, лог, мережу |
| TransformStream | Перетворювач | Стиснення, шифрування, фільтрація |
| pipeTo / pipeThrough | З'єднання потоків | readable.pipeThrough(transform).pipeTo(writable) |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.