Seriya: JavaScript Basics Dars 50

Async/Await

async/await Promise ustidagi sintaktik shakar ekani, async funksiya har doim Promise qaytarishi, await funksiyani generator kabi qanday 'muzlatishi', bu mexanizm V8'da generator+Promise+Event Loop kombinatsiyasi orqali ishlashi, va awaitlarni parallel ishlatish.

50-dars

Async/Await

1. Oddiy ta'rif

async/await — bu Promise ustidagi sintaktik shakar (syntactic sugar). Hech qanday yangi asinxron mexanizm emas — u faqat Promise zanjirini sinxron kod kabi ko'rinadigan shaklda yozish imkonini beradi.

// Promise bilan
function getData() {
  return fetch('/api/user')
    .then((res) => res.json())
    .then((data) => {
      console.log(data);
      return data;
    });
}

// async/await bilan — XUDDI SHU narsa, faqat boshqacha yozilgan
async function getData() {
  const res = await fetch('/api/user');
  const data = await res.json();
  console.log(data);
  return data;
}

Ikkalasi ham bir xil ishlaydi, bir xil microtask tartibida. Farq faqat — o'qish qulayligida.


2. async kalit so'zi nima qiladi?

Har qanday funksiya oldiga async qo'yilsa, ikkita narsa o'zgaradi:

  1. Funksiya har doim Promise qaytaradi — hatto ichida return orqali oddiy qiymat qaytarilsa ham
  2. Funksiya ichida await ishlatish imkoniyati paydo bo'ladi
async function foo() {
  return 5;
}

foo(); // Promise {<fulfilled>: 5} — 5 EMAS, Promise!
foo().then((val) => console.log(val)); // 5

Bu — avtomatik: return 5 yozilsa ham, V8 buni ichkarida return Promise.resolve(5) kabi ishlov beradi. Agar funksiya throw qilsa — qaytgan Promise avtomatik rejected bo'ladi:

async function bar() {
  throw new Error('xato');
}

bar().catch((err) => console.log(err.message)); // "xato"

3. await nima qiladi — bu yerda "sehr" boshlanadi

await — faqat Promise natijasini kutish uchun ishlatiladi, lekin muhim tomoni: u butun thread'ni bloklamaydi (agar bloklaganda edi, birinchi mavzuda ko'rgan "single-thread" tamoyili buzilardi). Buning o'rniga, u funksiyaning bajarilishini shu yerda "to'xtatadi" va boshqaruvni Call Stack'dan chiqarib yuboradi, keyin Promise hal bo'lganda — funksiya xuddi o'sha joydan davom etadi.

console.log('1');

async function run() {
  console.log('2');
  await null; // hatto oddiy qiymat bo'lsa ham, funksiya SHU YERDA to'xtaydi
  console.log('3'); // bu MICROTASK sifatida keyinroq bajariladi
}

run();
console.log('4');

Natija: 1, 2, 4, 3. Nega 4 — 3dan oldin chiqadi? Chunki awaitga yetganda, funksiya Call Stack'dan chiqib ketadi (garchi funksiya "tugallanmagan" bo'lsa ham!), boshqaruv qaytarib chaqiruvchi kodga (run()dan keyingi qator, ya'ni console.log('4')) beriladi. Faqat undan keyin, microtask sifatida, console.log('3') bajariladi.


4. V8 darajasida — bu qanday amalga oshadi? (Generator bilan bog'liqlik)

Bu — eng chuqur va muhim qism. async/await — V8 ichida haqiqatan ham generator funksiyalar va Promise'lar kombinatsiyasi orqali amalga oshiriladi.

Generator'ni eslang: function* — yield nuqtasida to'xtaydigan va davom ettiriladigan funksiya, uning holati (local o'zgaruvchilar, qayerda to'xtagani) xotirada saqlanadi:

function* genExample() {
  console.log('boshlandi');
  const x = yield 'pauza1';
  console.log('davom etdi, x =', x);
}

async functionni konseptual jihatdan shunday "qayta yozish" mumkin:

// async/await versiya
async function run() {
  const data = await fetchData();
  console.log(data);
}

// V8 ICHKARIDA (konseptual, soddalashtirilgan) — generator + "runner" orqali
function runAsync(generatorFn) {
  const generator = generatorFn();

  function step(value) {
    const { value: promiseOrValue, done } = generator.next(value);
    if (done) return;

    // Har doim Promise'ga o'raymiz (agar Promise bo'lmasa ham)
    Promise.resolve(promiseOrValue).then(
      (result) => step(result),          // muvaffaqiyat — davom ettiramiz
      (error) => generator.throw(error)  // xato — generator ichiga "otamiz"
    );
  }

  step();
}

function* run() {
  const data = yield fetchData(); // await → yield ga aylandi
  console.log(data);
}

runAsync(run);

Bu — haqiqiy V8 implementatsiyasi emas (V8 buni C++ darajasida, %AsyncFunctionAwait degan ichki operatsiyalar orqali qiladi), lekin mantiqiy jihatdan aynan shu tamoyilda ishlaydi: await — bu yieldga o'xshab, funksiyani "muzlatadi", tashqi "runner" (V8'ning o'z ichki mexanizmi) esa Promise hal bo'lishini kutib, keyin generatorni qayta ishga tushiradi (.next(value) chaqirib).


5. awaitdan keyingi kod — nima uchun MICROTASK

await promise yozilganda, V8 buni ichkarida taxminan quyidagicha ishlov beradi:

await someExpression;
// ↓ ichkarida (soddalashtirilgan)
1. someExpression Promise'ga o'raladi (agar Promise bo'lmasa ham,
   Promise.resolve(someExpression) kabi)
2. Joriy async funksiya — "muzlatiladi" (suspend), uning holati saqlanadi
3. Call Stack'dan chiqib ketiladi — chaqiruvchi kodga boshqaruv qaytadi
4. Wrapped Promise .then() ga o'xshash reaksiya qo'shiladi:
   "Promise hal bo'lganda, funksiyani DAVOM ETTIR"
5. Bu reaksiya — MICROTASK sifatida ishlaydi (Promise mexanizmi o'zi shunday)

Shuning uchun awaitdan keyingi har bir qator — kamida bitta microtask kechikish bilan bajariladi, hatto await qiymati allaqachon tayyor bo'lsa ham:

async function test() {
  console.log('A');
  await 1; // hatto "1" — allaqachon tayyor qiymat!
  console.log('B'); // baribir MICROTASK sifatida keyinroq
}

console.log('start');
test();
console.log('end');

// natija: start, A, end, B

6. Bir nechta await — nechta microtask "sakrash" bo'ladi?

Bu — amaliyotda muhim, kam biladigan nuance: har bir await — kamida bitta microtask tick qo'shadi, lekin ba'zan ikkitagacha (V8 versiyasiga va qiymat turiga qarab, ES2020'dan keyin optimallashtirilgan — oldin 3 tagacha bo'lardi):

async function a() {
  console.log('a: boshlandi');
  await b();
  console.log('a: davom etdi');
}

async function b() {
  console.log('b: boshlandi');
}

a();
console.log('sync tugadi');

Natija: a: boshlandi, b: boshlandi, sync tugadi, a: davom etdi. Sabab: b() chaqirilganda u darhol, sinxron ishga tushadi (b: boshlandi chiqadi) va Promise qaytaradi, lekin await b() shu Promise'ni kutish uchun a funksiyasini muzlatadi — shuning uchun sync tugadi undan oldin chiqadi.


7. Parallel bajarish — awaitni ketma-ket ishlatishning xatosi

Ko'p uchraydigan samarasizlik:

// YOMON — ketma-ket, umumiy vaqt = 2 + 2 = 4 sekund
async function slow() {
  const a = await fetchA(); // 2 sekund kutadi
  const b = await fetchB(); // FAQAT SHUNDAN KEYIN boshlanadi, yana 2 sekund
  return [a, b];
}

Bu yerda fetchB() — fetchA() tugaguncha umuman boshlanmaydi ham, garchi ular bir-biriga bog'liq bo'lmasa ham. To'g'ri yechim — ikkalasini bir vaqtda ishga tushirish:

// YAXSHI — parallel, umumiy vaqt = max(2, 2) = 2 sekund
async function fast() {
  const promiseA = fetchA(); // DARHOL ishga tushadi (Web API'ga topshiriladi)
  const promiseB = fetchB(); // BU HAM darhol ishga tushadi
  const a = await promiseA;  // faqat NATIJANI kutamiz
  const b = await promiseB;
  return [a, b];
}

Bu ishlaydi, chunki fetchA() va fetchB() — chaqirilgan zahoti (hali await qilinmasdan) ikkalasi ham o'z ishini (masalan, tarmoq so'rovini) boshlab yuboradi. await — faqat natijani "olish" nuqtasi, ishni "boshlash" nuqtasi emas.

Odatda Promise.all() bilan yozish — yanada tozaroq:

async function fast() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}

Xulosa — Async/Await = Generator + Promise + Event Loop birligi

async function     →  har doim Promise qaytaradi
await expr          →  1) expr Promise'ga o'raladi
                        2) funksiya "muzlatiladi" (generator kabi)
                        3) Call Stack bo'shaydi
                        4) Promise hal bo'lganda — MICROTASK sifatida
                           funksiya davom ettiriladi (generator.next() kabi)

Bu — hech qanday yangi asinxron "dvigatel" emas, balki allaqachon bor bo'lgan uch mexanizm (Generator'ning to'xtash/davom etish qobiliyati, Promise'ning holat boshqaruvi, Event Loop'ning Microtask navbati) ustiga qurilgan qulay yozish uslubi.