Асинхронний генератор
Асинхронний генератор (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 з пагінацією, ліниві завантаження, асинхронні пайплайни.
Швидкий приклад
async function* fetchNumbers() {
yield 1;
yield 2;
yield 3;
}
(async () => {
for await (const n of fetchNumbers()) {
console.log(n);
}
})();Вивід:
1
2
3Відмінність від звичайного function*: кожне значення повертається асинхронно, через Promise, тому працювати з ним можна лише через for await...of.
Асинхронний yield: очікування між кроками
Асинхронні генератори корисні саме тоді, коли між ітераціями є асинхронні дії: fetch, await, setTimeout, запити до бази даних і подібне.
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);
}
})();Вивід (з паузою в одну секунду між числами):
1
2
3Тут генератор асинхронно чекає всередині await, а потім віддає наступне значення через yield. Цикл for await...of на кожній ітерації неявно робить await над результатом .next().
Що повертає асинхронний генератор
Якщо крутити ітератор вручну, кожен .next() треба чекати:
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 }.
Асинхронний генератор повертає об’єкт із методом:
[Symbol.asyncIterator]()який повертає сам себе. Такий об’єкт можна перебирати лише через for await...of, а не через for...of.
Як і звичайний генератор, асинхронний підтримує делегування через yield*, тобто вкладення іншого асинхронного генератора:
async function* sub() {
yield 1;
yield 2;
}
async function* main() {
yield* sub(); // делегує ітерації
yield 3;
}
for await (const v of main()) {
console.log(v);
}Вивід:
1
2
3Працюють і методи керування .return() та .throw():
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
// Генератор завершеноПотік даних і асинхронні пайплайни
Асинхронні генератори ідеально підходять для потокової обробки даних:
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.
Кілька генераторів можна складати один в одного, як ланки конвеєра:
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-levelawait. - Очікувати паралельності. Асинхронний генератор послідовний: наступний
fetchстартує лише після того, як попереднє значення забрали. Для паралельних запитів потрібенPromise.all, а не генератор. - Ставити
yieldусерединіforEachабо іншого колбека.yieldпрацює тільки в тілі самого генератора, тому потрібні звичайні циклиforчиfor await...of. - Не закривати генератор. Якщо вийти з циклу через
breakабоreturn, рушій викличе.return(), тому ресурси (з’єднання, потоки) варто звільняти у блоціfinally.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.