В каталоге магазина 50 000 товаров. Загрузить их все разом — 10 секунд и Out of Memory. Нужна постраничная загрузка, но писать while с ручным управлением страницами в каждом месте потребления — дублирование кода. Асинхронный генератор скрывает логику пагинации внутри и даёт потребителю простой for await...of.
function*, yield, синхронная итерацияawait внутри асинхронного генератораnext() асинхронного итератора возвращает PromiseОбъект является асинхронно итерируемым, если у него есть метод [Symbol.asyncIterator], возвращающий объект с методом next(), который возвращает промис:
const paginated = {
[Symbol.asyncIterator]() {
let page = 1
return {
async next() {
if (page > 3) return { value: undefined, done: true }
const data = await fetchPage(page++)
return { value: data, done: false }
}
}
}
}for await (const page of paginated) {
console.log('Страница:', page.items)
}
// Ждёт каждую итерацию, прежде чем перейти к следующейЭто самый удобный способ создать асинхронный итератор:
async function* fetchPages(baseUrl) {
let page = 1
while (true) {
const res = await fetch(`${baseUrl}?page=${page}`)
const data = await res.json()
if (data.items.length === 0) return // конец данных
yield data.items
page++
}
}
for await (const items of fetchPages('/api/products')) {
renderItems(items)
}В отличие от синхронного генератора, async function* может использовать await перед yield:
async function* streamNumbers() {
for (let i = 1; i <= 5; i++) {
await new Promise(r => setTimeout(r, 200)) // пауза 200мс
yield i
}
}
// Числа появляются с задержкой
for await (const n of streamNumbers()) {
console.log(n) // 1, 2, 3, 4, 5 с паузами
}// Генератор страниц — потребитель не знает о пагинации
async function* allProducts(categoryId) {
let cursor = null
do {
const url = cursor
? `/api/products?category=${categoryId}&cursor=${cursor}`
: `/api/products?category=${categoryId}`
const res = await fetch(url)
const { products, nextCursor } = await res.json()
for (const product of products) {
yield product // отдаём по одному товару
}
cursor = nextCursor
} while (cursor)
}
// Потребитель работает просто, не думая о страницах
for await (const product of allProducts(5)) {
searchIndex.add(product)
}| | Синхронный function* | Асинхронный async function* |
|---|---|---|
| Объявление | function* gen() | async function* gen() |
| Внутри | yield value | yield value, await promise |
| Итерация | for...of | for await...of |
| next() возвращает | { value, done } | Promise<{ value, done }> |
| Применение | Синхронные данные | API, файлы, потоки |
for await...of корректно завершает генератор при break:
for await (const chunk of readLargeFile()) {
if (foundWhatWeNeed(chunk)) break // генератор получит сигнал и закроется
}1. Использование `for...of` вместо `for await...of` с асинхронным генератором:
async function* gen() { yield 1; yield 2 }
// Плохо: for...of не ждёт промисы — получаешь Promise объекты, а не значения
for (const item of gen()) {
console.log(item) // Promise { 1 }, Promise { 2 } — не то что ожидалось!
}
// Хорошо: for await...of автоматически awaits каждый next()
for await (const item of gen()) {
console.log(item) // 1, 2
}2. Забыть await перед вызовом async-функции внутри генератора — теряешь задержку:
// Плохо: без await функция возвращает Promise, а не значение
async function* badGen() {
const data = fetchPage(1) // забыли await!
yield data // отдаёт Promise, а не данные
}
// Хорошо:
async function* goodGen() {
const data = await fetchPage(1)
yield data // отдаёт уже разрешённые данные
}3. Нет обработки ошибок — первый упавший fetch ломает весь цикл:
// Плохо: если какая-то страница не загрузилась — вся итерация падает
async function* fetchPages() {
let page = 1
while (true) {
const data = await fetch(`/api/items?page=${page++}`).then(r => r.json())
if (!data.items.length) return
yield data.items
}
}
// Хорошо: оборачивай в try/catch внутри генератора
async function* fetchPagesSafe() {
let page = 1
while (true) {
try {
const data = await fetch(`/api/items?page=${page++}`).then(r => r.json())
if (!data.items.length) return
yield data.items
} catch (err) {
console.warn(`Страница ${page - 1} не загрузилась:`, err.message)
return // или continue для следующей страницы
}
}
}Асинхронный генератор с симуляцией постраничного API
// Симуляция серверной пагинации
const DATABASE = Array.from({ length: 23 }, (_, i) => ({
id: i + 1,
name: `Товар ${i + 1}`,
price: Math.round((i + 1) * 199.9),
}))
function simulateApiPage(page, pageSize = 5) {
return new Promise(resolve => {
setTimeout(() => {
const start = (page - 1) * pageSize
const items = DATABASE.slice(start, start + pageSize)
resolve({
items,
page,
totalPages: Math.ceil(DATABASE.length / pageSize),
hasMore: start + pageSize < DATABASE.length,
})
}, 30) // имитируем задержку сети
})
}
// Асинхронный генератор: выдаёт товары постранично
async function* fetchAllProducts(pageSize = 5) {
let page = 1
let hasMore = true
while (hasMore) {
const result = await simulateApiPage(page, pageSize)
console.log(`Загружена страница ${result.page}/${result.totalPages}`)
yield result.items
hasMore = result.hasMore
page++
}
}
// Потребитель: обходим все страницы через for await...of
async function main() {
let totalProducts = 0
let totalRevenue = 0
for await (const pageItems of fetchAllProducts(7)) {
totalProducts += pageItems.length
totalRevenue += pageItems.reduce((sum, p) => sum + p.price, 0)
console.log(` Обработано товаров в странице: ${pageItems.length}`)
}
console.log(`\nВсего товаров: ${totalProducts}`)
console.log(`Суммарная выручка: ${totalRevenue.toLocaleString('ru-RU')} ₽`)
}
main()В каталоге магазина 50 000 товаров. Загрузить их все разом — 10 секунд и Out of Memory. Нужна постраничная загрузка, но писать while с ручным управлением страницами в каждом месте потребления — дублирование кода. Асинхронный генератор скрывает логику пагинации внутри и даёт потребителю простой for await...of.
function*, yield, синхронная итерацияawait внутри асинхронного генератораnext() асинхронного итератора возвращает PromiseОбъект является асинхронно итерируемым, если у него есть метод [Symbol.asyncIterator], возвращающий объект с методом next(), который возвращает промис:
const paginated = {
[Symbol.asyncIterator]() {
let page = 1
return {
async next() {
if (page > 3) return { value: undefined, done: true }
const data = await fetchPage(page++)
return { value: data, done: false }
}
}
}
}for await (const page of paginated) {
console.log('Страница:', page.items)
}
// Ждёт каждую итерацию, прежде чем перейти к следующейЭто самый удобный способ создать асинхронный итератор:
async function* fetchPages(baseUrl) {
let page = 1
while (true) {
const res = await fetch(`${baseUrl}?page=${page}`)
const data = await res.json()
if (data.items.length === 0) return // конец данных
yield data.items
page++
}
}
for await (const items of fetchPages('/api/products')) {
renderItems(items)
}В отличие от синхронного генератора, async function* может использовать await перед yield:
async function* streamNumbers() {
for (let i = 1; i <= 5; i++) {
await new Promise(r => setTimeout(r, 200)) // пауза 200мс
yield i
}
}
// Числа появляются с задержкой
for await (const n of streamNumbers()) {
console.log(n) // 1, 2, 3, 4, 5 с паузами
}// Генератор страниц — потребитель не знает о пагинации
async function* allProducts(categoryId) {
let cursor = null
do {
const url = cursor
? `/api/products?category=${categoryId}&cursor=${cursor}`
: `/api/products?category=${categoryId}`
const res = await fetch(url)
const { products, nextCursor } = await res.json()
for (const product of products) {
yield product // отдаём по одному товару
}
cursor = nextCursor
} while (cursor)
}
// Потребитель работает просто, не думая о страницах
for await (const product of allProducts(5)) {
searchIndex.add(product)
}| | Синхронный function* | Асинхронный async function* |
|---|---|---|
| Объявление | function* gen() | async function* gen() |
| Внутри | yield value | yield value, await promise |
| Итерация | for...of | for await...of |
| next() возвращает | { value, done } | Promise<{ value, done }> |
| Применение | Синхронные данные | API, файлы, потоки |
for await...of корректно завершает генератор при break:
for await (const chunk of readLargeFile()) {
if (foundWhatWeNeed(chunk)) break // генератор получит сигнал и закроется
}1. Использование `for...of` вместо `for await...of` с асинхронным генератором:
async function* gen() { yield 1; yield 2 }
// Плохо: for...of не ждёт промисы — получаешь Promise объекты, а не значения
for (const item of gen()) {
console.log(item) // Promise { 1 }, Promise { 2 } — не то что ожидалось!
}
// Хорошо: for await...of автоматически awaits каждый next()
for await (const item of gen()) {
console.log(item) // 1, 2
}2. Забыть await перед вызовом async-функции внутри генератора — теряешь задержку:
// Плохо: без await функция возвращает Promise, а не значение
async function* badGen() {
const data = fetchPage(1) // забыли await!
yield data // отдаёт Promise, а не данные
}
// Хорошо:
async function* goodGen() {
const data = await fetchPage(1)
yield data // отдаёт уже разрешённые данные
}3. Нет обработки ошибок — первый упавший fetch ломает весь цикл:
// Плохо: если какая-то страница не загрузилась — вся итерация падает
async function* fetchPages() {
let page = 1
while (true) {
const data = await fetch(`/api/items?page=${page++}`).then(r => r.json())
if (!data.items.length) return
yield data.items
}
}
// Хорошо: оборачивай в try/catch внутри генератора
async function* fetchPagesSafe() {
let page = 1
while (true) {
try {
const data = await fetch(`/api/items?page=${page++}`).then(r => r.json())
if (!data.items.length) return
yield data.items
} catch (err) {
console.warn(`Страница ${page - 1} не загрузилась:`, err.message)
return // или continue для следующей страницы
}
}
}Асинхронный генератор с симуляцией постраничного API
// Симуляция серверной пагинации
const DATABASE = Array.from({ length: 23 }, (_, i) => ({
id: i + 1,
name: `Товар ${i + 1}`,
price: Math.round((i + 1) * 199.9),
}))
function simulateApiPage(page, pageSize = 5) {
return new Promise(resolve => {
setTimeout(() => {
const start = (page - 1) * pageSize
const items = DATABASE.slice(start, start + pageSize)
resolve({
items,
page,
totalPages: Math.ceil(DATABASE.length / pageSize),
hasMore: start + pageSize < DATABASE.length,
})
}, 30) // имитируем задержку сети
})
}
// Асинхронный генератор: выдаёт товары постранично
async function* fetchAllProducts(pageSize = 5) {
let page = 1
let hasMore = true
while (hasMore) {
const result = await simulateApiPage(page, pageSize)
console.log(`Загружена страница ${result.page}/${result.totalPages}`)
yield result.items
hasMore = result.hasMore
page++
}
}
// Потребитель: обходим все страницы через for await...of
async function main() {
let totalProducts = 0
let totalRevenue = 0
for await (const pageItems of fetchAllProducts(7)) {
totalProducts += pageItems.length
totalRevenue += pageItems.reduce((sum, p) => sum + p.price, 0)
console.log(` Обработано товаров в странице: ${pageItems.length}`)
}
console.log(`\nВсего товаров: ${totalProducts}`)
console.log(`Суммарная выручка: ${totalRevenue.toLocaleString('ru-RU')} ₽`)
}
main()Напиши асинхронный генератор range(from, to, delayMs), который выдаёт числа от from до to включительно, делая паузу delayMs миллисекунд перед каждым значением. Используй его с for await...of для вывода чисел в консоль.
await new Promise(resolve => setTimeout(resolve, delayMs)) создаёт паузу. После неё используй yield i для выдачи значения. Цикл for...of в тесте 3 прервётся через break, и генератор будет автоматически закрыт.