Back

Сценарии использования генераторов JavaScript

Сценарии использования генераторов JavaScript

Генераторы JavaScript (function*) являются частью языка с версии ES2015, однако многие frontend-разработчики по-прежнему используют массивы или цепочки промисов там, где генераторы были бы более подходящим решением. Основная ценность заключается не в производительности — а в ленивых вычислениях, композируемости и точном контроле над итерацией. В этой статье рассматриваются случаи, когда генераторы действительно находят своё место в современном frontend-коде.

Ключевые выводы

  • Генераторы создают значения по требованию через ленивые вычисления, избегая ненужных вычислений и промежуточных массивов
  • Iterator Helpers API предоставляет встроенные методы map, filter и take для итераторов, возвращаемых генераторами, устраняя необходимость в пользовательских вспомогательных функциях
  • yield* делает рекурсивный обход деревьев и графов одновременно читаемым и ленивым
  • Асинхронные генераторы (async function*) в паре с for await...of обрабатывают постраничную или пакетную загрузку данных с минимальным управлением состоянием

Чем генераторы JavaScript отличаются от других конструкций

Функция-генератор возвращает итератор. Вызов функции не выполняет никакого кода — он передаёт вам объект с методом .next(). Каждый вызов .next() выполняет тело функции до следующего yield, затем приостанавливается, сохраняя локальное состояние между вызовами.

function* range(start, end) {
  for (let i = start; i < end; i++) yield i
}

for (const n of range(0, 5)) {
  console.log(n) // 0, 1, 2, 3, 4
}

Поскольку генераторы реализуют протокол итератора, они работают напрямую с for...of, синтаксисом spread и деструктуризацией — адаптер не требуется.

Ленивая итерация в JavaScript: обработка данных без их материализации

Основная причина использовать генератор вместо массива — это ленивая итерация: значения создаются только по запросу. Это важно, когда:

  • Полный набор данных велик, а вам нужна только его часть
  • Вычисление каждого значения требует больших затрат
  • Последовательность концептуально бесконечна
function* naturals() {
  let n = 0
  while (true) yield n++
}

// Вычисляет значения только до точки прерывания
for (const n of naturals()) {
  if (n > 100) break
}

Промежуточный массив не создаётся. Значения после точки прерывания не вычисляются.

Iterator Helpers API: встроенные ленивые конвейеры

Написание пользовательских утилит map, filter и take раньше было необходимым шаблонным кодом. Iterator Helpers API — теперь доступный во всех современных браузерах — добавляет эти методы напрямую к синхронным итераторам:

const result = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray() // [0, 4, 16, 36, 64]

Каждый шаг является ленивым. .toArray() — это то, что запускает вычисление. Это делает конвейеры на основе генераторов значительно чище без сторонних библиотек. Обратите внимание, что эти помощники применяются к синхронным итераторам — помощники для асинхронных итераторов ещё не стандартизированы во всех окружениях.

Обход деревьев и графов

Генераторы естественным образом подходят для обхода рекурсивных структур. Обход дерева в глубину для DOM-подобной структуры становится простым:

function* walkTree(node) {
  yield node
  for (const child of node.children ?? []) {
    yield* walkTree(child)
  }
}

for (const node of walkTree(rootNode)) {
  if (node.type === 'input') processInput(node)
}

yield* делегирует вложенному генератору, сохраняя рекурсию читаемой, а обход — ленивым: вы останавливаетесь, как только находите то, что нужно.

Асинхронные генераторы в JavaScript: постраничная и пакетная загрузка данных

async function* расширяет паттерн на асинхронные последовательности. В сочетании с for await...of он хорошо подходит для постраничных ответов API:

async function* fetchPages(url) {
  let nextUrl = url
  while (nextUrl) {
    const res = await fetch(nextUrl)
    const data = await res.json()
    yield data.items
    nextUrl = data.nextPage ?? null
  }
}

for await (const batch of fetchPages('/api/records')) {
  processBatch(batch)
}

Каждая страница загружается только при переходе цикла к следующей итерации. Нет необходимости собирать все страницы заранее или управлять состоянием пагинации извне — генератор хранит его.

Когда не стоит использовать генераторы

Генераторы добавляют косвенность. Для простого преобразования массива, который вы полностью обработаете, цепочки методов массива более понятны. Используйте генераторы, когда последовательность большая, потенциально бесконечная или когда вам нужно остановиться досрочно без напрасных вычислений.

Заключение

Генераторы JavaScript проявляют себя в трёх областях: ленивая итерация по большим или бесконечным последовательностям, композируемые конвейеры данных (особенно с Iterator Helpers API) и асинхронная загрузка данных, где требуется последовательный контроль с сохранением состояния. Они не заменяют массивы или async/await — это правильный инструмент, когда нужно создавать значения по требованию, а не все сразу.

Часто задаваемые вопросы

Да. Генераторы хорошо работают для создания последовательностей данных, которые потребляют React-компоненты. Вы можете вызвать генератор внутри хука useEffect или useMemo для ленивого вычисления значений. Однако не используйте генератор в качестве самой функции компонента — React ожидает, что компоненты вернут JSX, а не итераторы.

Генератор останется приостановленным в последней точке yield. Он станет доступен для сборки мусора, как только не останется ссылок на его объект-итератор. Если вам нужна логика очистки при досрочной остановке итерации, оберните yield в блок try-finally. Блок finally выполнится, когда будет вызван метод return итератора или генератор будет собран сборщиком мусора.

Для небольших, полностью обрабатываемых коллекций генераторы имеют небольшие накладные расходы из-за механизма приостановки и возобновления. Разница в производительности незначительна в большинстве приложений. Генераторы становятся быстрее на практике, когда вы частично обрабатываете большие наборы данных, потому что они избегают выделения промежуточных массивов и пропускают вычисления для значений, которые вы никогда не запрашиваете.

Асинхронный генератор выдаёт значения постепенно по мере их появления, в то время как подход на основе Promise ждёт, пока все данные будут собраны, прежде чем вернуть результат. Это означает, что асинхронные генераторы позволяют начать обработку первого пакета результатов немедленно, снизить пиковое использование памяти и дают более точный контроль над тем, когда происходит каждая последующая загрузка.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay