Generatorlar, ilg'or iteratsiya

Asinxron iteratsiya va generatorlar

Asinxron iteratsiya bizga ma'lumot asinxron ravishda, ya'ni Promise lar orqali qismlar bo'lib kelganda uni birma-bir qayta ishlashga imkon beradi. Masalan, tarmoqdan sahifama-sahifa yuklanadigan ma'lumot โ€” bu asinxron iteratsiya uchun ideal holat.

Oddiy iteratsiyani eslaylik

Avval sinxron (oddiy) iteratsiyani eslaymiz. Iterativ obyekt yaratish uchun unga Symbol.iterator metodini qo'shamiz:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,

      next() {
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (let value of range) {
  console.log(value); // 1, 2, 3, 4, 5
}

Bu yerda barcha qadamlar sinxron โ€” next() darhol tayyor {value, done} obyektini qaytaradi. Kutish yo'q. Ammo hayotda ko'p ma'lumotlar vaqt talab qilib keladi: tarmoqdan yuklab olish, fayl o'qish, ma'lumotlar bazasidan so'rov. Bunday holatlar uchun asinxron iteratsiya kerak.

Sinxron va asinxron iteratsiya farqi

Asinxron iteratsiya sinxronidan uch nuqtada farq qiladi. Farqlarni jadval kabi solishtiramiz:

Asinxron iteratorda next() Promise qaytargani uchun, for await..of har bir qadamda avtomatik ravishda await qiladi โ€” ya'ni Promise bajarilishini kutadi, so'ng keyingi qadamga o'tadi.
Asinxron iterativlar bilan spread sintaksisi ... ishlamaydi โ€” u Symbol.iterator (sinxron) ni kutadi, Symbol.asyncIterator ni emas. Xuddi shunday, oddiy for..of ham asinxron iterativ bilan ishlamaydi.

Symbol.asyncIterator

Obyektni asinxron iterativ qilish uchun unga Symbol.iterator emas, Symbol.asyncIterator metodini qo'shamiz. Uning next() metodi Promise qaytarishi (yoki async bo'lishi) kerak.

Quyidagi misolda range obyektini asinxron qilamiz โ€” har qiymat 1 sekundlik kechikish bilan keladi:

let range = {
  from: 1,
  to: 5,

  // Symbol.iterator O'RNIGA Symbol.asyncIterator
  [Symbol.asyncIterator]() {
    return {
      current: this.from,
      last: this.to,

      // next() endi async โ€” Promise qaytaradi
      async next() {
        // 1 sekund kutamiz (asinxronlikni taqlid qilamiz)
        await new Promise(resolve => setTimeout(resolve, 1000));

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {
  // for..of EMAS, for AWAIT..of
  for await (let value of range) {
    console.log(value); // 1, keyin 2, ... har biri 1 sek kechikish bilan
  }
})();

E'tibor bering: next() ichida await ishlatilgani uchun u async deb belgilandi, va sikl for await..of bo'ldi. Har bir iteratsiya Promise bajarilishini kutadi.

for await..of faqat async funksiya ichida ishlatilishi mumkin (yoki top-level modul kontekstida). Oddiy sinxron funksiya ichida uni ishlata olmaysiz.

Asinxron generatorlar (async function*)

Oddiy generatorlar sinxron edi โ€” ular ichida await ishlatib bo'lmaydi va barcha qiymatlar sinxron chiqadi. Asinxron generator esa async function* orqali yaratiladi va uning ichida await ishlatish mumkin.

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    // Endi await ishlata olamiz!
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

(async () => {
  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    console.log(value); // 1, 2, 3, 4, 5 โ€” har biri 1 sek kechikish bilan
  }
})();

Sinxron va asinxron generatorlar orasidagi asosiy farqlar:

Asinxron generatorda next() ni qo'lda chaqirsangiz, u Promise qaytaradi. Shuning uchun uni await qilish kerak: let res = await generator.next(); โ€” bu holda res {value, done} bo'ladi.
async function* gen() {
  yield 1;
  yield 2;
}

(async () => {
  let g = gen();
  console.log(await g.next()); // {value: 1, done: false}
  console.log(await g.next()); // {value: 2, done: false}
  console.log(await g.next()); // {value: undefined, done: true}
})();

Generator orqali asinxron iterativ obyekt

Sinxron dunyodagidek, asinxron obyektlarni ham generator yordamida ancha soddalashtirish mumkin. Symbol.asyncIterator ni asinxron generator qilib e'lon qilamiz:

let range = {
  from: 1,
  to: 5,

  // async generator sifatida qisqa yozuv
  async *[Symbol.asyncIterator]() {
    for (let value = this.from; value <= this.to; value++) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      yield value;
    }
  }
};

(async () => {
  for await (let value of range) {
    console.log(value); // 1, 2, 3, 4, 5 (kechikish bilan)
  }
})();

Bu variant qo'lda next(), current, last yozishdan ancha qisqa va o'qishga oson. Generator holatni (joriy qiymat, sikl joyi) o'zida avtomatik saqlaydi.

Umumiy qoida: agar iteratsiya mantig'i murakkab bo'lsa yoki holatni qo'lda boshqarish zerikarli bo'lsa โ€” generator (sinxron yoki asinxron) yozing. U kodni sezilarli darajada soddalashtiradi.

Amaliy misol: sahifama-sahifa ma'lumot yuklash

Asinxron generatorlar eng ko'p sahifalash (pagination) uchun ishlatiladi. Ko'p API'lar ma'lumotni bo'laklarga (sahifalarga) bo'lib beradi: har so'rov bir sahifa qaytaradi, unda keyingi sahifa manzili (URL) bo'ladi. Asinxron generator bu jarayonni chiroyli inkapsulyatsiya qiladi:

// Tasavvur qiling: bu funksiya URL bo'yicha bir sahifani yuklaydi
// va { items: [...], next: 'keyingi-url yoki null' } qaytaradi.
async function* fetchCommits(url) {
  while (url) {
    const response = await fetch(url); // sahifani so'raymiz
    const body = await response.json();

    // keyingi sahifa manzilini olamiz
    url = body.next || null;

    // sahifadagi har bir elementni birma-bir yield qilamiz
    for (let item of body.items) {
      yield item;
    }
  }
}

(async () => {
  let count = 0;
  for await (let commit of fetchCommits('https://api.example.com/commits')) {
    console.log(commit);
    if (++count >= 100) break; // faqat dastlabki 100 ta kerak
  }
})();

Bu kodning go'zalligi shundaki โ€” foydalanuvchi (for await..of yozgan kod) sahifalash haqida umuman o'ylamaydi. Generator sahifalarni orqa fonda, kerak bo'lganda yuklaydi. break qilinsa, keyingi sahifalar umuman so'ralmaydi โ€” bu tarmoq trafigini tejaydi.

Bu โ€” dangasa (lazy) yuklashning kuchli namunasi: ma'lumot faqat for await..of keyingi qiymat so'raganda yuklanadi. Foydalanuvchi 100 elementdan keyin to'xtasa, million elementli manba ham to'liq yuklanmaydi.

Qachon asinxron iteratsiya kerak?

Asinxron iteratsiyani qachon tanlash kerakligini aniq belgilaymiz:

Agar ma'lumotingiz allaqachon xotirada tayyor bo'lsa (oddiy massiv, string, Map) โ€” asinxron iteratsiya kerak emas, oddiy for..of yetarli va tezroq.

Qoidani sodda eslang: agar iteratsiyaning har bir qadami kutishni (Promise) talab qilsa โ€” asinxron iteratsiya (for await..of, async function*) ishlating. Aks holda โ€” oddiy iteratsiya.

Muhim nozikliklar va tuzoqlar

Katta xatolik: asinxron generator ichida await ishlatishni unutmang. Agar asinxronlik kerak bo'lmasa, umuman oddiy (sinxron) generator yozing โ€” async ortiqcha yuk qo'shadi.

Xulosa