Асинхронные итераторы и генераторы

for await, async function* и асинхронные итераторы — как обрабатывать потоки данных, приходящие частями.

Асинхронный итератор — объект, чей метод next() возвращает промис; перебирается циклом for await...of.

Зачем нужны

Обычный итератор отдаёт значения синхронно. Но данные часто приходят порциями во времени: страницы API, строки из сети, события. Асинхронные итераторы позволяют писать for await (const item of source) и получать элементы по мере готовности, не собирая всё в память сразу.

async function* — асинхронный генератор

Генератор с async может использовать await внутри и yield для выдачи значений. Каждое значение «созревает» асинхронно:

async function* range(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, 5));  // имитация асинхронного источника
    yield i;
  }
}

async function main() {
  for await (const n of range(1, 4)) {
    console.log("получено:", n);
  }
  console.log("поток завершён");
}
main();

Вывод:

получено: 1
получено: 2
получено: 3
получено: 4
поток завершён

Постраничная загрузка — типичный сценарий

Представьте API, отдающий данные страницами. Асинхронный генератор инкапсулирует логику «запроси страницу, отдай элементы, перейди к следующей», а потребитель просто перебирает результат:

// имитация API: возвращает страницу из 2 элементов, пока не дойдём до конца
async function fetchPage(page) {
  await new Promise(r => setTimeout(r, 3));
  const data = { 1: ["a", "b"], 2: ["c", "d"], 3: [] };
  return data[page] || [];
}

async function* paginate() {
  let page = 1;
  while (true) {
    const items = await fetchPage(page);
    if (items.length === 0) break;
    for (const item of items) yield item;
    page++;
  }
}

async function main() {
  const collected = [];
  for await (const item of paginate()) {
    collected.push(item);
  }
  console.log("все элементы:", collected.join(", "));
}
main();

Вывод:

все элементы: a, b, c, d

Symbol.asyncIterator вручную

Под капотом for await ищет метод [Symbol.asyncIterator]. Свой асинхронно-итерируемый объект можно собрать и без генератора:

const asyncCountdown = {
  from: 3,
  [Symbol.asyncIterator]() {
    let current = this.from;
    return {
      async next() {
        await new Promise(r => setTimeout(r, 5));
        if (current > 0) return { value: current--, done: false };
        return { value: undefined, done: true };
      }
    };
  }
};

async function main() {
  for await (const n of asyncCountdown) {
    console.log("тик:", n);
  }
  console.log("пуск!");
}
main();

Вывод:

тик: 3
тик: 2
тик: 1
пуск!

Итог

  • async function* комбинирует await и yield для потоков данных во времени.
  • for await...of перебирает асинхронный источник по мере готовности элементов.
  • Под капотом — метод [Symbol.asyncIterator], чей next() возвращает промис.
Проверьте себя
1. Какой цикл перебирает асинхронный итератор?
Afor...of
Bfor await...of
Cfor...in
Dwhile с await внутри
2. Что возвращает метод next() асинхронного итератора?
AСразу объект { value, done }
BПромис, разрешающийся в { value, done }
CТолько value
DМассив значений
3. Что комбинирует async function*?
AТолько yield
Bawait и yield
CТолько await
DPromise.all и yield
Поддержать проект