Асинхронные итераторы #
Асинхронный итератор реализует [Symbol.asyncIterator]() и возвращает объект, у которого next() отдаёт Promise на { value, done }. Цикл for await...of и async-генераторы async function* — основной способ последовательно обрабатывать данные из промисов и fetch без ручной цепочки then.
for await...of
#
Перебирает async iterable — каждый шаг ждёт разрешения Promise:
async function loadOrderStatuses(ids) {
const results = []
for (const id of ids) {
const res = await fetch(`/api/orders/${id}`)
results.push(await res.json())
}
return results
}
Когда источник уже async iterable:
const statuses = {
async *[Symbol.asyncIterator]() {
yield await Promise.resolve('pending')
yield await Promise.resolve('shipped')
yield await Promise.resolve('delivered')
},
}
for await (const status of statuses) {
console.log(status)
}
Async-генератор async function*
#
Сочетает yield с await внутри тела:
async function fetchOrderPage(page) {
const res = await fetch(`/api/orders?page=${page}`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
async function* allOrders() {
let page = 1
while (true) {
const { items, hasMore } = await fetchOrderPage(page)
yield* items
if (!hasMore) return
page++
}
}
for await (const order of allOrders()) {
console.log(order.id)
}
Каждый yield приостанавливает генератор до следующего запроса next() в for await — память не заполняется всем каталогом сразу.
Отличие от синхронного генератора #
Синхронный function* | Async async function* | |
|---|---|---|
next() возвращает | { value, done } | Promise<{ value, done }> |
await в теле | нельзя | можно |
| Цикл | for...of | for await...of |
| Symbol | Symbol.iterator | встроенный async iterator |
Синхронный генератор с промисами внутри без async потребует ручного await на каждом next() — async-генератор скрывает эту обёртку.
Ручной async-итератор #
function createDeliveryEvents(socket) {
const queue = []
let resolveWait = null
socket.on('event', (ev) => {
if (resolveWait) {
resolveWait({ value: ev, done: false })
resolveWait = null
} else {
queue.push(ev)
}
})
return {
[Symbol.asyncIterator]() {
return {
async next() {
if (queue.length) {
return { value: queue.shift(), done: false }
}
return new Promise((resolve) => {
resolveWait = (result) => resolve(result)
})
},
}
},
}
}
На практике чаще используют async function* или готовые абстракции (ReadableStream и т.д.).
Обработка ошибок #
Ошибка внутри async-генератора прерывает for await...of, если не перехвачена:
async function* flakyCheckout() {
yield await loadCart()
throw new Error('оплата недоступна')
yield await confirmOrder()
}
try {
for await (const step of flakyCheckout()) {
console.log(step)
}
} catch (e) {
console.error(e.message)
}
Паттерн как в try/catch с async/await — один try вокруг цикла или внутри итерации.
Параллельность и осторожность #
for await...of выполняет итерации последовательно. Для параллельной загрузки страниц — Promise.all или ограниченный пул:
async function* expressSlots(ids, concurrency = 2) {
const pending = new Set()
for (const id of ids) {
const task = fetch(`/api/slots/${id}`).then((r) => r.json())
pending.add(task)
task.finally(() => pending.delete(task))
if (pending.size >= concurrency) {
yield await Promise.race(pending)
}
}
while (pending.size) {
yield await Promise.race(pending)
}
}
Выбор между последовательным async-итератором и параллельным batch — вопрос нагрузки на API и порядка результатов.
Практика: поток обновлений доставки #
async function* trackShipment(trackingId) {
let lastEvent = null
while (true) {
const res = await fetch(`/api/tracking/${trackingId}`)
const { events, completed } = await res.json()
const fresh = events.filter((e) => e.id !== lastEvent?.id)
for (const event of fresh) {
lastEvent = event
yield event
}
if (completed) return
await new Promise((r) => setTimeout(r, 5000))
}
}
for await (const event of trackShipment('TRK-9X2')) {
console.log(event.label)
}
async function collectPlaylistFromApi(slug) {
const tracks = []
for await (const track of fetchTracksPages(slug)) {
tracks.push(track)
if (tracks.length >= 50) break
}
return tracks
}
async function* fetchTracksPages(slug) {
let cursor = null
do {
const q = cursor ? `?cursor=${cursor}` : ''
const data = await fetch(`/api/playlists/${slug}/tracks${q}`).then((r) => r.json())
yield* data.items
cursor = data.nextCursor
} while (cursor)
}
Итог #
- Async iterable:
next()→Promise<{ value, done }>; обход —for await...of. async function*объединяетyieldиawaitдля ленивых асинхронных последовательностей.- Подходит для пагинации API, потоков статусов и пошагового чекаута с сетевыми вызовами.
- Для параллельных запросов комбинируйте с
Promise.all; async-итератор по умолчанию последователен.