Streams API
Streams API nima, ReadableStream/WritableStream/TransformStream turlari, reader.read() orqali chunk-based o'qish, .text()/.json()/.blob() metodlarining ichki mexanizmi, TextDecoder, pipeThrough zanjiri, backpressure va pull model, hamda browser engine darajasida qanday ishlashi.
87-dars
Streams API
fetch()ni eslang:
const res = await fetch('/big-video.mp4');
const data = await res.blob(); // BUTUN fayl to'liq yuklanguncha kutadi
Bu yerda muammo: agar fayl 500MB bo'lsa, res.blob() hamma narsa to'liq kelmaguncha hech narsa qaytarmaydi. Xotirada butun fayl bir vaqtda saqlanishi kerak, va foydalanuvchi hech narsa ko'rmaydi, to hammasi tugamaguncha.
Streams API — bu muammoni yechadi: data'ni bir butun blok sifatida emas, balki kichik bo'laklar (chunk) oqimi sifatida qayta ishlash imkonini beradi — xuddi trubadan suv oqib turgani kabi, hammasi bir vaqtda kelishini kutmasdan, har bir tomchi kelgan zahoti ishlatish mumkin.
1. Uchta asosiy turi
| Tur | Vazifasi |
|---|---|
| ReadableStream | Data manbai — undan o'qiysiz (masalan, network javobi, fayl) |
| WritableStream | Data qabul qiluvchi — unga yozasiz (masalan, faylga saqlash) |
| TransformStream | Ikkalasi bir vaqtda — kirgan data'ni o'zgartirib, chiqaradi (masalan, siqish, shifrlash) |
2. ReadableStream — asosiy misol
const response = await fetch('/big-file.txt');
const reader = response.body.getReader(); // ReadableStream'dan reader olamiz
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log('Chunk keldi:', value); // value — Uint8Array (bayt massivi)
}
Bu yerda response.body — bu butun javob emas, balki ReadableStream obyekti. fetch chaqirilganda, server javobi hali to'liq kelmagan bo'lishi mumkin — lekin siz allaqachon reader.read() orqali kelgan qismini darhol olishingiz mumkin, qolgani kelayotganda kutmasdan.
{ done, value } — har bir read() chaqiruvi shu formatda javob beradi:
done: false, value: Uint8Array(...)— hali data bordone: true, value: undefined— stream tugadi
Nega bu FOYDALI — real misol: yuklab olish foizi
async function downloadWithProgress(url) {
const response = await fetch(url);
const total = Number(response.headers.get('content-length'));
let loaded = 0;
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
loaded += value.length;
console.log(`Yuklandi: ${((loaded / total) * 100).toFixed(1)}%`);
}
return new Blob(chunks);
}
response.blob() bilan bu mumkin emas edi — chunki u sizga faqat oxirida, bitta yakuniy natija beradi. Streams orqali esa har bir chunk kelganda darhol reaksiya berish mumkin — progress bar, real-time preview va hokazo.
3. .text(), .json(), .blob() aslida nima qiladi
Bu yerda muhim tushuncha bor: fetch javobidagi .text(), .json(), .blob() metodlari — bu Streams API ustiga qurilgan qulaylik (convenience) metodlar:
// .text() aslida ICHKARIDA shunday qiladi:
async function myText(response) {
const reader = response.body.getReader();
let result = '';
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
}
return result;
}
Ya'ni .text() — bu shunchaki stream'ni to'liq o'qib, bitta string'ga yig'ib beruvchi yordamchi funksiya. Siz xom Streams API'ni ishlatganda, aynan shu ishni o'zingiz boshqarasiz — va shu orqali memory'ni tejash yoki progressiv ishlov berish imkoniga ega bo'lasiz.
4. Muhim: Uint8Array va TextDecoder
Stream orqali kelayotgan value — raw baytlar (Uint8Array), matn emas. Uni o'qish uchun TextDecoder kerak:
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(value, { stream: true });
{ stream: true } muhim — chunki UTF-8'da bitta belgi (masalan, ⌘) bir nechta baytdan iborat bo'lishi mumkin, va agar chunk chegarasi aynan shu belgi o'rtasida kesib o'tsa, decoder bu holatni eslab qolib, keyingi chunk kelganda davom ettiradi — aks holda matn buzilib ketardi.
5. WritableStream — teskari yo'nalish
const writableStream = new WritableStream({
write(chunk) {
console.log('Yozildi:', chunk);
// masalan, bu yerda faylga yoki serverga yuborish mumkin
},
close() {
console.log('Stream yopildi');
},
abort(err) {
console.error('Xato bilan to\'xtadi:', err);
}
});
const writer = writableStream.getWriter();
writer.write('salom');
writer.write('dunyo');
writer.close();
Bu — teskari tomon: siz data'ni bo'lak-bo'lak yozasiz, va write(chunk) handler har safar avtomatik chaqiriladi.
6. TransformStream — ikkalasini bog'lash
const upperCaseTransform = new TransformStream({
transform(chunk, controller) {
const text = new TextDecoder().decode(chunk);
controller.enqueue(new TextEncoder().encode(text.toUpperCase()));
}
});
Bu — kirgan chunk'ni o'zgartirib, chiqishga yuboradi. Buning eng kuchli tomoni — pipeThrough orqali stream'larni zanjir qilib bog'lash:
const response = await fetch('/data.txt');
response.body
.pipeThrough(new TextDecoderStream()) // baytlarni matnga aylantiradi
.pipeThrough(upperCaseTransform) // katta harfga o'giradi
.pipeTo(someWritableStream); // yakuniy manzilga yozadi
Bu — Unix pipe (cat file | grep x | sort) konsepsiyasiga to'g'ridan-to'g'ri o'xshaydi: har bir bosqich o'z vazifasini bajaradi, va data bir marta ham to'liq xotiraga yig'ilmasdan, oqim sifatida bir bosqichdan ikkinchisiga o'tadi.
7. Chuqurroq — Backpressure (eng muhim texnik tushuncha)
Bu — Streams API'ning eng chuqur va muhim g'oyasi. Muammo: agar production (data ishlab chiqaruvchi) tezroq ishlasa, consumptiondan (data iste'mol qiluvchidan) — masalan, tarmoqdan data juda tez kelayapti, lekin diskka yozish sekin — unda data xotirada to'planib ketadi (memory bloat, hatto crash).
Backpressure — bu iste'molchi "sekinlashtir, men hali tayyor emasman" deb ishlab chiqaruvchiga signal berish mexanizmi:
const writableStream = new WritableStream({
write(chunk) {
return new Promise(resolve => {
// sekin operatsiya, masalan diskka yozish
setTimeout(() => {
console.log('Yozildi:', chunk);
resolve(); // faqat SHU yerda keyingi chunk so'raladi
}, 1000);
});
}
});
write() Promise qaytarganda, stream mexanizmi avtomatik ravishda keyingi chunk'ni yubormaydi, to bu Promise resolve bo'lguncha. Bu — butun zanjir bo'ylab (pipeThrough, pipeTo) avtomatik tarqaladi: agar oxirgi WritableStream sekin bo'lsa, bu signal orqaga, TransformStream orqali, hatto original ReadableStream'gacha (masalan, TCP darajasida — server yuborishni sekinlashtiradi) yetib boradi.
Bu — aynan TCP protokolining o'zidagi flow control g'oyasi bilan bir xil: browser darajasida ham, xuddi shunday, "iste'molchi band — kutib tur" signali ishlaydi.
8. V8 va browser engine darajasida nima sodir bo'ladi
ReadableStream, WritableStream — bular native, C++ darajasida (V8'ning o'zida emas, balki brauzer engine'ning umumiy qismida — Chromium'da third_party/blink/renderer/core/streams) amalga oshirilgan, lekin spetsifikatsiyasi to'liq JS-semantikasida yozilgan (WHATWG Streams Standard — bu spetsifikatsiya hujjat aslida pseudocode JS ko'rinishida yozilgan, noyob holat).
Network layer (C++) → data chunk keladi
→ Bu chunk browser'ning ICHKI buferiga qo'yiladi
→ ReadableStream controller.enqueue() chaqiradi (bu C++dan V8'ga signal)
→ V8 ichida bu — Promise resolve bilan teng: reader.read() kutayotgan
Promise shu yerda bajariladi
→ Sizning JS kodingiz (onmessage o'rniga, await orqali) davom etadi
Muhim farq — Web Worker/WebSocket'dagi event-based modeldan farqli, Streams API to'liq Promise-based, pull modelda ishlaydi: siz o'zingiz reader.read() chaqirib, "menga keyingi chunk kerak" deb so'raysiz (pull), server/network esa faqat siz so'raganda javob beradi — bu ham backpressure'ning tabiiy natijasi (WebSocket'dagi kabi "push" emas).
9. Real hayotdagi qo'llanilishi
- Katta fayllarni yuklab olish/yuklash progress bilan
- Video/audio streaming (YouTube kabi — butun video kutilmaydi)
- Katta CSV/JSON'ni bo'lak-bo'lak parse qilish
TextEncoderStream/TextDecoderStreamorqali real-time matn kodlash- Service Worker'da katta javoblarni kesh bilan bir vaqtda foydalanuvchiga uzatish (
response.body.pipeThrough)