Асинхронные итераторы и генераторы в JavaScript
Асинхронные итераторы и генераторы в JavaScript: Symbol.asyncIterator, async function*, for await...of и пример ленивой постраничной загрузки.
Асинхронное программирование — краеугольный камень современной разработки на JavaScript. Оно позволяет писать неблокирующий, конкурентный код, эффективно обрабатывающий сетевые запросы, файловый ввод-вывод и таймеры. Это руководство посвящено асинхронным итераторам и асинхронным генераторам — двум возможностям, появившимся в ECMAScript 2018, которые позволяют перебирать данные, поступающие по мере готовности: один фрагмент за сетевой round-trip, одно событие на действие пользователя — без блокировки остальной программы.
Предполагается, что вы уже знакомы с обычными итерируемыми объектами, генераторами и промисами. Если что-то из этого вам незнакомо, прочитайте сначала соответствующие разделы.
Понимание асинхронных итераторов
Что такое асинхронные итераторы?
Асинхронные итераторы — особый вид итераторов, предназначенный для работы с асинхронными потоками данных. В отличие от традиционных синхронных итераторов, асинхронные позволяют перебирать последовательности асинхронных значений — промисов или потоков — без блокировки выполнения.
С технической точки зрения, объект считается асинхронно итерируемым, если он реализует метод Symbol.asyncIterator, возвращающий объект-итератор. Вот практический пример ручной реализации этого интерфейса на пользовательском объекте:
Синхронные и асинхронные итераторы
Разница между обычным и асинхронным итерируемым объектом сводится к трём вещам: имени метода, типу значения, возвращаемого next(), и циклу для перебора.
| Синхронный итерируемый | Асинхронный итерируемый | |
|---|---|---|
| Метод | Symbol.iterator | Symbol.asyncIterator |
next() возвращает | { value, done } | Promise от { value, done } |
| Цикл потребления | for...of | for await...of |
| Синтаксис генератора | function* | async function* |
Поскольку в асинхронном случае next() возвращает Promise, каждый шаг цикла может ожидать завершения асинхронной операции — fetch-запроса, таймера, чтения из базы данных — прежде чем будет получено следующее значение. Обычный for...of на это не способен: он ожидает немедленной доступности value и done. Использование for await...of на синхронно итерируемом объекте по-прежнему работает (движок оборачивает значения в выполненные промисы), однако применение синхронного for...of к асинхронному итерируемому объекту не работает — вы просто получите перебор объектов pending Promise.
Как использовать асинхронные итераторы
Чтобы применять асинхронные итераторы в своём JavaScript-коде, нужно прежде всего разобраться с их основными концепциями и синтаксисом. Рассмотрим простой пример, демонстрирующий принцип работы асинхронных итераторов на практике:
В этом примере мы определяем асинхронную функцию-генератор generateNumbers(), которая последовательно выдаёт числа асинхронно. Затем из функции-генератора создаётся асинхронный итерируемый объект, и цикл for await...of перебирает значения, производимые асинхронным итератором.
Примечание: Когда вы yield обычное значение внутри async function*, метод next() итератора автоматически возвращает Promise, который разрешается в { value: <ваше значение>, done: false }. Явно yield-ить Promise нужно только в том случае, если вы хотите, чтобы потребитель получил объект Promise, а не разрешённое значение.
Практическое применение асинхронных итераторов
Асинхронные итераторы широко применяются в сценариях, связанных с асинхронной обработкой данных: загрузка данных из внешних API, чтение из потоков, обработка асинхронных событий. Их универсальность и эффективность делают их незаменимым инструментом для современных JavaScript-разработчиков, создающих масштабируемые и отзывчивые приложения.
Знакомство с генераторами JavaScript
Введение в генераторы
Генераторы — мощная возможность, появившаяся в ECMAScript 2015, позволяющая создавать итерируемые последовательности с произвольной логикой итерации. В отличие от обычных функций, которые выполняются до конца при вызове, генераторы могут приостанавливать и возобновлять своё выполнение, обеспечивая ленивое вычисление значений.
Важно различать стандартные и асинхронные генераторы:
- Стандартные генераторы (
function*): возвращают значения синхронно. - Асинхронные генераторы (
async function*): возвращают Promise при каждом вызовеnext(), позволяя потребителю ожидать каждое значение с помощьюfor await...of.
Применение генераторов для асинхронного программирования
Один из наиболее привлекательных вариантов использования генераторов — асинхронное программирование. Сочетая генераторы с промисами, разработчики могут создавать асинхронные рабочие процессы, которые одновременно элегантны и легко воспринимаются. Вот современный пример использования асинхронного генератора для получения и возврата данных с удалённого сервера:
В этом примере мы определяем асинхронную функцию-генератор fetchTodos(), которая асинхронно загружает данные из удалённого API с помощью функции fetch(). Используя await внутри генератора и возвращая отдельные элементы через yield, мы можем передавать результаты прямо в цикл for await...of без ручных вызовов .next() и цепочек промисов.
Постраничная загрузка с асинхронным генератором
Шаблон, демонстрирующий все преимущества асинхронных генераторов, — ленивая пагинация. Многие API возвращают результаты постранично и требуют запрашивать следующую страницу до тех пор, пока данные не закончатся. Асинхронный генератор позволяет скрыть всю эту логику: он запрашивает страницу, поочерёдно возвращает её элементы и запрашивает следующую страницу только тогда, когда потребитель запрашивает ещё данные. Вызывающий код может прервать итерацию досрочно — например, найдя нужное — и никаких дополнительных сетевых запросов сделано не будет.
Обратите внимание на break: поскольку генератор ленивый, выход из цикла после 25 элементов означает, что генератор никогда не запросит третью страницу. Именно это отличает асинхронный генератор от предварительной загрузки всего массива данных — вы платите только за те данные, которые реально используете.
Продвинутые шаблоны генераторов
Генераторы предлагают множество продвинутых шаблонов и техник для решения сложных задач программирования. Вот несколько примеров, демонстрирующих их универсальность:
- Параллельное выполнение: запуская несколько генераторов и управляя их промисами конкурентно, можно выполнять несколько асинхронных задач одновременно.
- Обработка ошибок: используйте блоки
try-catchвнутри генераторов для корректной обработки отклонённых промисов, возникающих в процессе итерации. - Конвейеры данных: создавайте конвейеры обработки данных, объединяя генераторы в цепочку, где результат одного генератора служит входными данными для следующего.
Заключение
Асинхронные итераторы и генераторы — незаменимые инструменты в арсенале современного JavaScript-разработчика. Освоив эти мощные возможности, вы откроете новые измерения выразительности и эффективности в асинхронном коде. Разрабатываете ли вы веб-приложения, серверные API или утилиты командной строки — асинхронные итераторы и генераторы помогут справиться со сложными асинхронными задачами с лёгкостью. Начните применять их в своих JavaScript-проектах уже сегодня и выведите свои навыки программирования на новый уровень!
Связанные темы
- Итерируемые объекты — синхронная основа за
for...ofиSymbol.iterator. - Генераторы — синтаксис
function*, на котором строятся асинхронные генераторы. - Промисы — то, что разрешает каждый
awaitвнутри асинхронного генератора. - Async/await — синтаксис, с которым используется
for await...of.