Skip to main content

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

Асинхронний генератор (async function*) це функція, яка поступово повертає асинхронні значення. Вона створює асинхронний ітератор, метод .next() якого повертає Promise, а не звичайний об’єкт { value, done }.

Теорія

TL;DR

  • Оголошення: async function* name() { ... }, тобто async плюс зірочка.
  • Повертає асинхронний ітератор; .next() дає Promise<{ value, done }>.
  • Усередині дозволені і await, і yield, тому між кроками можна робити запити, паузи, читання з бази.
  • Перебирається тільки через for await...of; звичайний for...of з ним не працює, бо реалізовано [Symbol.asyncIterator].
  • Підтримує делегування через yield*, а також методи керування .return() і .throw().
  • Типове застосування: стрімінг даних, API з пагінацією, ліниві завантаження, асинхронні пайплайни.

Швидкий приклад

javascript
async function* fetchNumbers() { yield 1; yield 2; yield 3; } (async () => { for await (const n of fetchNumbers()) { console.log(n); } })();

Вивід:

javascript
1 2 3

Відмінність від звичайного function*: кожне значення повертається асинхронно, через Promise, тому працювати з ним можна лише через for await...of.

Асинхронний yield: очікування між кроками

Асинхронні генератори корисні саме тоді, коли між ітераціями є асинхронні дії: fetch, await, setTimeout, запити до бази даних і подібне.

javascript
async function* delayedNumbers() { for (let i = 1; i <= 3; i++) { await new Promise(resolve => setTimeout(resolve, 1000)); // чекаємо 1 секунду yield i; } } (async () => { for await (const n of delayedNumbers()) { console.log(n); } })();

Вивід (з паузою в одну секунду між числами):

javascript
1 2 3

Тут генератор асинхронно чекає всередині await, а потім віддає наступне значення через yield. Цикл for await...of на кожній ітерації неявно робить await над результатом .next().

Що повертає асинхронний генератор

Якщо крутити ітератор вручну, кожен .next() треба чекати:

javascript
const gen = delayedNumbers(); console.log(await gen.next()); // { value: 1, done: false } console.log(await gen.next()); // { value: 2, done: false } console.log(await gen.next()); // { value: 3, done: false } console.log(await gen.next()); // { value: undefined, done: true }

gen.next() повертає Promise, який резолвиться в { value, done }.

Асинхронний генератор повертає об’єкт із методом:

javascript
[Symbol.asyncIterator]()

який повертає сам себе. Такий об’єкт можна перебирати лише через for await...of, а не через for...of.

Як і звичайний генератор, асинхронний підтримує делегування через yield*, тобто вкладення іншого асинхронного генератора:

javascript
async function* sub() { yield 1; yield 2; } async function* main() { yield* sub(); // делегує ітерації yield 3; } for await (const v of main()) { console.log(v); }

Вивід:

javascript
1 2 3

Працюють і методи керування .return() та .throw():

javascript
async function* gen() { try { yield 1; yield 2; } catch (e) { console.log('Помилка всередині:', e.message); } finally { console.log('Генератор завершено'); } } const iterator = gen(); console.log(await iterator.next()); // { value: 1, done: false } console.log(await iterator.throw(new Error("fail"))); // Помилка всередині: fail // Генератор завершено

Потік даних і асинхронні пайплайни

Асинхронні генератори ідеально підходять для потокової обробки даних:

javascript
async function* streamData(urls) { for (const url of urls) { const res = await fetch(url); const data = await res.json(); yield data; } } (async () => { const urls = [ '/api/user', '/api/posts', '/api/comments' ]; for await (const chunk of streamData(urls)) { console.log('Дані:', chunk); } })();

Тут кожен fetch() виконується по черзі, а після кожного yield дані віддаються назовні, у цикл for await...of.

Кілька генераторів можна складати один в одного, як ланки конвеєра:

javascript
async function* generateNumbers() { for (let i = 1; i <= 10; i++) { await new Promise(r => setTimeout(r, 200)); yield i; } } async function* filterEven(source) { for await (const n of source) { if (n % 2 === 0) yield n; } } async function* double(source) { for await (const n of source) { yield n * 2; } } // Комбінуємо кілька асинхронних генераторів: (async () => { const pipeline = double(filterEven(generateNumbers())); for await (const value of pipeline) { console.log(value); // 4, 8, 12, 16, 20 } })();

Це і є лінива асинхронна обробка даних, по суті потокова пайплайн-система (stream pipeline): нічого не обчислюється наперед, кожна ланка тягне наступне значення лише тоді, коли його попросили.

Де це застосовують і підсумок

СценарійЯк використовується
Стрімінг данихПоступове завантаження частин файлу або відповіді сервера
Поступові запитиРобота з API, яке повертає дані «сторінками»
Ліниві обчисленняГенерація значень у міру потреби
Асинхронні пайплайниКомбінування кількох джерел даних
Node.js StreamsСумісність з інтерфейсами ReadableStream та AsyncIterator

Підсумок:

ОсобливістьОпис
Оголошенняasync function* name() { ... }
ПовертаєАсинхронний ітератор
Метод .next()Повертає Promise<{ value, done }>
Використовується зfor await...of
yieldАсинхронно віддає проміжні значення
ЗастосуванняПотоки даних, API з пагінацією, ліниві завантаження, генерація подій

Головна ідея коротко: function* дає синхронні послідовності, а async function* дає асинхронні, наприклад «по одному запиту за раз».

Типові помилки

  • Перебирати асинхронний генератор через for...of. Буде помилка, бо об’єкт реалізує [Symbol.asyncIterator], а не [Symbol.iterator]; потрібен for await...of.
  • Забувати await перед .next(). Без нього ви отримаєте Promise, а не { value, done }, і перевірка done завжди буде undefined.
  • Використовувати for await...of поза асинхронним контекстом. Він працює лише всередині async-функції або в модулі з підтримкою top-level await.
  • Очікувати паралельності. Асинхронний генератор послідовний: наступний fetch стартує лише після того, як попереднє значення забрали. Для паралельних запитів потрібен Promise.all, а не генератор.
  • Ставити yield усередині forEach або іншого колбека. yield працює тільки в тілі самого генератора, тому потрібні звичайні цикли for чи for await...of.
  • Не закривати генератор. Якщо вийти з циклу через break або return, рушій викличе .return(), тому ресурси (з’єднання, потоки) варто звільняти у блоці finally.

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

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

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