← JavaScript/Асинхронные итераторы и генераторы#125 из 383← ПредыдущийСледующий →+35 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

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

В каталоге магазина 50 000 товаров. Загрузить их все разом — 10 секунд и Out of Memory. Нужна постраничная загрузка, но писать while с ручным управлением страницами в каждом месте потребления — дублирование кода. Асинхронный генератор скрывает логику пагинации внутри и даёт потребителю простой for await...of.

На основе предыдущих уроков

  • «Генераторы» — function*, yield, синхронная итерация
  • «Итерируемые» — Symbol.iterator, протокол итерации
  • «async/await» — await внутри асинхронного генератора
  • «Промисы» — next() асинхронного итератора возвращает Promise
  • Symbol.asyncIterator — асинхронный итерируемый объект

    Объект является асинхронно итерируемым, если у него есть метод [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...of — обход асинхронного потока

    for await (const page of paginated) {
      console.log('Страница:', page.items)
    }
    // Ждёт каждую итерацию, прежде чем перейти к следующей

    async function* — асинхронный генератор

    Это самый удобный способ создать асинхронный итератор:

    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)
    }

    yield с await внутри

    В отличие от синхронного генератора, 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)
    }

    Sync vs Async генераторы

    | | Синхронный 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, файлы, потоки |

    Ранний выход и finally

    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 без раскрытия логики пагинации потребителю
  • Стриминг — Node.js Readable streams реализуют Symbol.asyncIterator
  • WebSocket — обёртка над WebSocket как асинхронный итератор сообщений
  • Батчинг задач — обрабатывать большие очереди задач по частям, не загружая память
  • Примеры

    Асинхронный генератор с симуляцией постраничного 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, синхронная итерация
  • «Итерируемые» — Symbol.iterator, протокол итерации
  • «async/await» — await внутри асинхронного генератора
  • «Промисы» — next() асинхронного итератора возвращает Promise
  • Symbol.asyncIterator — асинхронный итерируемый объект

    Объект является асинхронно итерируемым, если у него есть метод [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...of — обход асинхронного потока

    for await (const page of paginated) {
      console.log('Страница:', page.items)
    }
    // Ждёт каждую итерацию, прежде чем перейти к следующей

    async function* — асинхронный генератор

    Это самый удобный способ создать асинхронный итератор:

    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)
    }

    yield с await внутри

    В отличие от синхронного генератора, 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)
    }

    Sync vs Async генераторы

    | | Синхронный 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, файлы, потоки |

    Ранний выход и finally

    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 без раскрытия логики пагинации потребителю
  • Стриминг — Node.js Readable streams реализуют Symbol.asyncIterator
  • WebSocket — обёртка над WebSocket как асинхронный итератор сообщений
  • Батчинг задач — обрабатывать большие очереди задач по частям, не загружая память
  • Примеры

    Асинхронный генератор с симуляцией постраничного 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, и генератор будет автоматически закрыт.

    Загружаем среду выполнения...
    Загружаем AI-помощника...