Skip to main content

Як працюють streams (ReadableStream, WritableStream)?

Що таке потік (Stream)

Потік (Stream) - це послідовність даних, яка надходить поступово, а не вся одразу.

Головна відмінність від звичайних даних:

  • звичайний fetch().text() чекає, поки все прийде → потім повертає результат;
  • потік (fetch().body) дозволяє обробляти дані по шматочках, одразу в міру надходження (chunk by chunk).

Типи потоків у JavaScript

ТипПризначення
ReadableStreamджерело даних (читання)
WritableStreamприймач даних (запис)
TransformStreamпроміжна обробка (читання + запис)
DuplexStreamдвосторонній (наприклад, сокет)

Приклад: читання даних з потоку (ReadableStream)

Уяви, ти робиш запит на великий файл:

javascript
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); }

Що відбувається:

  1. fetch повертає Response, у якого .body - це ReadableStream.
  2. Метод .getReader() створює читача, який повертає дані порціями (chunks).
  3. reader.read() повертає проміс з { done, value }.
  • value - шматочок даних (тип Uint8Array),
  • done: true - потік закінчився.

Так можна завантажувати гігабайти, не завантажуючи все в оперативну пам'ять.

Приклад: запис даних у WritableStream

javascript
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 (перетворення потоку)

javascript
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) як водопровідні труби

javascript
await readableStream .pipeThrough(transformStream) .pipeTo(writableStream);

Таким чином:

  • pipeThrough застосовує перетворення;
  • pipeTo передає результат наступному потоку.

Приклад з fetch() та TextDecoderStream

javascript
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 тощо), але концепція та сама:

javascript
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)

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.