Пошук уроків, статей та іншого контенту
Дослідите асинхронні ітератори та генератори для послідовного читання потокових або порційних даних.
Звичайний ітератор повертає значення синхронно:
const iterator = [10, 20][Symbol.iterator]();
iterator.next(); // { value: 10, done: false }
iterator.next(); // { value: 20, done: false }
iterator.next(); // { value: undefined, done: true }Асинхронний ітератор має такий самий загальний протокол, але його метод next() повертає Promise:
const result = await asyncIterator.next();
// { value: ..., done: false }Це дає змогу послідовно обробляти дані, які надходять із:
мережевих запитів;
файлів або потоків;
баз даних;
черг повідомлень;
API з пагінацією;
WebSocket-подібних джерел;
генераторів, що виконують асинхронні операції між значеннями.
Асинхронний ітерований об’єкт повинен реалізувати метод із ключем Symbol.asyncIterator. Цей метод повертає асинхронний ітератор із методом next().
for await...ofНайзручніше споживати асинхронні ітератори за допомогою for await...of:
for await (const item of source) {
console.log(item);
}На кожній ітерації JavaScript:
викликає source[Symbol.asyncIterator]();
викликає метод next();
очікує його результат;
перевіряє властивість done;
передає value у тіло циклу;
переходить до наступного значення.
Тіло циклу також може містити await:
for await (const user of users) {
const profile = await loadProfile(user.id);
console.log(profile);
}Наступний виклик next() відбудеться лише після завершення поточної ітерації. Тому for await...of природно забезпечує послідовну обробку та обмежує кількість одночасно активних операцій.
Розглянемо власну реалізацію джерела порційних даних:
class PageIterator {
constructor(pages, delay = 300) {
this.pages = pages;
this.delay = delay;
this.index = 0;
}
[Symbol.asyncIterator]() {
return this;
}
async next() {
if (this.index >= this.pages.length) {
return {
value: undefined,
done: true,
};
}
await new Promise((resolve) => {
setTimeout(resolve, this.delay);
});
const value = this.pages[this.index];
this.index += 1;
return {
value,
done: false,
};
}
}
async function main() {
const pages = new PageIterator([
["Alice", "Bob"],
["Carol", "David"],
["Eve"],
]);
for await (const page of pages) {
console.log("Отримано сторінку:", page);
}
}
main().catch(console.error);У цьому прикладі:
Symbol.asyncIterator повертає сам об’єкт;
next() є асинхронним методом;
результатом кожного кроку є об’єкт із value і done;
після завершення всіх сторінок повертається { done: true }.
Метод Symbol.asyncIterator не обов’язково повинен повертати this. Він може повертати окремий об’єкт-ітератор:
const source = {
values: [1, 2, 3],
[Symbol.asyncIterator]() {
let index = 0;
return {
async next() {
if (index >= source.values.length) {
return { done: true };
}
return {
value: source.values[index++],
done: false,
};
},
};
},
};Ручна реалізація next() часто має багато шаблонного коду. Асинхронні генератори спрощують створення асинхронних ітераторів.
Асинхронний генератор оголошується за допомогою async function*:
async function* numbers() {
await delay(200);
yield 1;
await delay(200);
yield 2;
await delay(200);
yield 3;
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function main() {
for await (const number of numbers()) {
console.log(number);
}
}
main().catch(console.error);Виклик асинхронного генератора не запускає його тіло одразу. Він повертає асинхронний ітерований об’єкт.
Кожен yield:
призупиняє виконання генератора;
формує чергове значення;
завершується після того, як споживач запросить наступне значення.
На відміну від звичайного генератора, результат next() асинхронного генератора є промісом:
async function* values() {
yield "first";
yield "second";
}
async function inspect() {
const iterator = values();
console.log(await iterator.next());
// { value: "first", done: false }
console.log(await iterator.next());
// { value: "second", done: false }
console.log(await iterator.next());
// { value: undefined, done: true }
}
inspect().catch(console.error);Типовий сценарій — API повертає дані сторінками. Асинхронний генератор може приховати механізм пагінації від коду, який споживає дані.
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function fetchPage(pageNumber) {
await delay(250);
const pages = {
1: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
2: [
{ id: 3, name: "Carol" },
{ id: 4, name: "David" },
],
3: [],
};
return pages[pageNumber] ?? [];
}
async function* fetchAllUsers() {
let pageNumber = 1;
while (true) {
const page = await fetchPage(pageNumber);
if (page.length === 0) {
return;
}
for (const user of page) {
yield user;
}
pageNumber += 1;
}
}
async function main() {
for await (const user of fetchAllUsers()) {
console.log(user.id, user.name);
}
}
main().catch(console.error);Код, який споживає дані, не знає:
скільки сторінок існує;
як формується номер сторінки;
коли потрібно виконати наступний запит;
як об’єднати сторінки в один послідовний потік.
Генератор видає користувачів по одному, хоча джерело отримує їх порціями.
yield та yield*Звичайний yield повертає одне значення:
async function* oneValue() {
yield "value";
}Оператор yield* делегує виконання іншому ітерованому або асинхронно ітерованому об’єкту:
async function* firstSource() {
yield 1;
yield 2;
}
async function* secondSource() {
yield* firstSource();
yield 3;
yield 4;
}
async function main() {
for await (const value of secondSource()) {
console.log(value);
}
}
main().catch(console.error);Результат:
1
2
3
4yield* зручний для композиції кількох джерел:
async function* allSources() {
yield* sourceFromDatabase();
yield* sourceFromCache();
yield* sourceFromFile();
}Джерела оброблятимуться послідовно: спочатку повністю завершиться перше, потім почнеться друге.
Асинхронні генератори добре підходять для конвеєрів обробки даних. Один генератор читає значення, інший фільтрує їх, а третій перетворює.
async function* numbers() {
for (let number = 1; number <= 10; number += 1) {
await delay(50);
yield number;
}
}
async function* onlyEven(source) {
for await (const number of source) {
if (number % 2 === 0) {
yield number;
}
}
}
async function* square(source) {
for await (const number of source) {
yield number ** 2;
}
}
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function main() {
const pipeline = square(onlyEven(numbers()));
for await (const value of pipeline) {
console.log(value);
}
}
main().catch(console.error);Послідовність виконання для кожного значення має вигляд:
numbers → onlyEven → square → споживачПри цьому конвеєр працює ліниво. Наступне число не обробляється, доки споживач не попросить наступний результат.
Це важлива відмінність від підходу з Promise.all, який може одразу запустити велику кількість операцій і спожити багато пам’яті.
Помилка, яка виникла в асинхронному генераторі або всередині циклу, стає відхиленим промісом.
async function* unreliableSource() {
yield "початок";
throw new Error("Джерело даних недоступне");
}
async function main() {
try {
for await (const value of unreliableSource()) {
console.log(value);
}
} catch (error) {
console.error("Помилка:", error.message);
}
}
main().catch(console.error);Якщо джерело може тимчасово повертати помилки, рішення залежить від вимог:
завершити весь потік і передати помилку вище;
повторити конкретний запит;
пропустити пошкоджену порцію;
повернути спеціальний об’єкт із результатом і помилкою.
Не варто бездумно перехоплювати всі помилки всередині генератора. Це може приховати критичний збій і створити потік неповних даних.
finallyАсинхронний генератор може мати блок finally для звільнення ресурсів:
async function* resourceStream() {
console.log("Відкриття ресурсу");
try {
yield "дані 1";
yield "дані 2";
yield "дані 3";
} finally {
console.log("Закриття ресурсу");
}
}
async function main() {
for await (const value of resourceStream()) {
console.log(value);
if (value === "дані 2") {
break;
}
}
}
main().catch(console.error);При виході з for await...of через break JavaScript намагається завершити ітератор, викликаючи його метод return(). Для асинхронного генератора це призводить до виконання finally.
Це особливо важливо для:
закриття файлу;
завершення транзакції;
звільнення підключення;
скасування підписки;
видалення тимчасових ресурсів.
Однак коректне звільнення ресурсів має бути реалізоване самим ітератором або генератором. Цикл не може автоматично знати, як закривати зовнішній ресурс.
Іноді потрібно отримувати значення не через for await...of, а вручну:
async function* messages() {
yield "message 1";
yield "message 2";
}
async function main() {
const iterator = messages();
try {
while (true) {
const result = await iterator.next();
if (result.done) {
break;
}
console.log(result.value);
}
} finally {
await iterator.return();
}
}
main().catch(console.error);Методи асинхронного генератора:
next(value) — продовжити виконання і отримати наступне значення;
return(value) — завершити генератор;
throw(error) — передати помилку всередину генератора.
Параметр value у next(value) може бути отриманий генератором як результат виразу yield:
async function* conversation() {
const answer = yield "Я поставив запитання";
yield `Ви відповіли: ${answer}`;
}
async function main() {
const iterator = conversation();
console.log(await iterator.next());
console.log(await iterator.next("Так"));
console.log(await iterator.next());
}
main().catch(console.error);Перший аргумент першого next() ігнорується, оскільки до першого yield ще немає призупиненого виразу, якому можна передати значення.
for await...offor await...of може працювати не лише з асинхронними, а й зі звичайними ітерованими об’єктами:
async function main() {
for await (const value of [1, 2, 3]) {
console.log(value);
}
}
main().catch(console.error);Для звичайного ітерованого об’єкта JavaScript обгортає результати в проміси. Це зручно, якщо функція повинна приймати як синхронні, так і асинхронні джерела.
Водночас така сумісність не робить синхронний об’єкт асинхронним. Якщо його next() блокує виконання, for await...of не усуне це блокування.
Потік даних часто виробляє значення швидше, ніж споживач їх обробляє. Асинхронна ітерація дає змогу організувати зворотний тиск:
споживач запитує одне значення;
джерело читає або готує його;
споживач обробляє значення;
лише після цього запитується наступне.
Це допомагає уникати необмеженого накопичення даних у пам’яті.
Проте зворотний тиск працює лише тоді, коли джерело справді очікує запит next(). Якщо генератор заздалегідь запускає тисячі операцій або складає всі результати у великий масив, перевага лінивої ітерації втрачається.
Такий код виконує операції послідовно:
for await (const item of source) {
await process(item);
}Це безпечно для операцій, порядок яких важливий, але може бути повільним.
Якщо операції незалежні, можна обмежено використовувати паралельність. Наприклад, спочатку зібрати невелику порцію:
async function processBatch(source, batchSize) {
const batch = [];
for await (const item of source) {
batch.push(item);
if (batch.length === batchSize) {
await Promise.all(batch.map(processItem));
batch.length = 0;
}
}
if (batch.length > 0) {
await Promise.all(batch.map(processItem));
}
}
async function processItem(item) {
// Імітація незалежної асинхронної операції
await new Promise((resolve) => setTimeout(resolve, 100));
console.log("Оброблено:", item);
}Такий підхід обмежує кількість одночасних операцій розміром порції. Вибір розміру залежить від:
пропускної здатності зовнішнього сервісу;
доступної пам’яті;
вимог до порядку результатів;
обмежень API;
вартості помилок і повторних спроб.
for...of замість for await...offor (const value of asyncSource) {
// Помилка: асинхронне джерело не є звичайним ітерованим об’єктом
}Для асинхронного джерела потрібен for await...of.
await під час ручної ітераціїconst result = iterator.next();
if (result.done) {
// Неправильно: result є Promise
}Правильний варіант:
const result = await iterator.next();
if (result.done) {
// Тепер result — об'єкт результату ітерації
}const allItems = [];
for await (const item of source) {
allItems.push(item);
}Це може бути виправдано для невеликого набору даних, але небезпечно для необмеженого або дуже великого потоку. Краще обробляти значення одразу або використовувати обмежені порції.
yield і returnasync function* source() {
return [1, 2, 3];
}Такий генератор не видасть три значення. Масив буде фінальним значенням завершеного генератора.
Для видачі кожного елемента потрібно написати:
async function* source() {
yield 1;
yield 2;
yield 3;
}Або:
async function* source() {
yield* [1, 2, 3];
}Promise.allPromise.all над усіма елементами потоку може одночасно запустити тисячі операцій. Для великих потоків використовуйте послідовну обробку або обмежену паралельність.
Якщо генератор читає зовнішній ресурс, необхідно передбачити finally або коректну реалізацію return(). Особливо це важливо, коли споживач виходить із циклу через break, помилку або скасування.
Асинхронний ітерований об’єкт реалізує Symbol.asyncIterator.
Його next() повертає проміс із результатом { value, done }.
for await...of спрощує послідовне читання асинхронних даних.
Асинхронні генератори оголошуються через async function*.
yield видає значення, а yield* делегує ітерацію іншому джерелу.
Асинхронні генератори зручні для пагінації, потоків і конвеєрів обробки.
break у for await...of може завершити ітератор через return().
Блок finally допомагає звільняти зовнішні ресурси.
Послідовна ітерація забезпечує зворотний тиск, але за потреби її можна доповнити обмеженою паралельністю.
Під час роботи з великими потоками не слід безконтрольно накопичувати значення або запускати всі операції через Promise.all.