В JavaScript async/await
стал стандартом для асинхронного кода. Но когда задачи становятся сложнее – множественные источники событий, отмена операций, стримы данных – генераторы предлагают уникальные возможности, недоступные при стандартном подходе. Рассмотрим их потенциал за пределами создания итераторов.
Принцип работы: Прерываемые функции
Генераторная функция (function*
) может приостанавливать выполнение с помощью yield
и возобновляться позже. Каждый вызов next()
возвращает объект { value, done }
.
function* simpleGen() {
yield 'First';
const midValue = yield 'Second';
yield midValue.toUpperCase();
}
const gen = simpleGen();
console.log(gen.next()); // { value: 'First', done: false }
console.log(gen.next()); // { value: 'Second', done: false }
console.log(gen.next('result')); // { value: 'RESULT', done: false }
console.log(gen.next()); // { value: undefined, done: true }
Ключевые моменты:
yield
возвращает значение в вызывающий код- Метод
next()
может передать значение внутрь генератора (становится результатомyield
) - Состояние функции (локальные переменные, позиция) сохраняется между вызовами
Асинхронные корутины: До async/await
Можно имитировать поведение async/await
, используя генераторы для управления промисами. Рассмотрим утилиту run
:
function run(generatorFn) {
const gen = generatorFn();
function handle(result) {
if (result.done) return result.value;
return result.value
.then(data => handle(gen.next(data)))
.catch(err => handle(gen.throw(err)));
}
return handle(gen.next());
}
Использование:
run(function* fetchUser() {
try {
const user = yield fetch('/api/user').then(r => r.json());
const posts = yield fetch(`/api/posts/${user.id}`).then(r => r.json());
return { user, posts };
} catch (error) {
console.error("Fetch failed:", error);
throw error;
}
}).then(data => console.log(data));
Сильные стороны генераторов: За пределами async/await
- Двусторонняя коммуникация
В отличие от линейного
await
, генератор позволяет получать данные извне во время работы:
function* userInteraction() {
const userId = yield { type: 'WAIT_INPUT' };
const user = yield fetchUser(userId);
yield { type: 'RENDER_USER', user };
}
// Драйвер
const gen = userInteraction();
gen.next(); // Запуск, запрос input
// Когда пользователь вводит ID:
const userId = 42;
gen.next(userId); // Продолжаем выполнение с полученным ID
- Комплексная отмена операций
yield
может передавать управление наружу для обработки прерываний:
function* cancellableTask() {
const abortController = new AbortController();
try {
const promise = fetch(url, { signal: abortController.signal });
yield { cancel: () => abortController.abort() };
return yield promise;
} finally {
// Очистка при отмене
abortController.abort();
}
}
// Внешний код с доступом к генератору:
const task = cancellableTask();
const cancel = task.next().value.cancel; // Получаем колбэк для отмены
cancel(); // Прерываем операцию
- Генерация и потребление данных по требованию Идеально для работы со стримами без нагрузки на память:
async function* streamReader(response) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
}
// Потребление порциями через цикл `for await`
for await (const chunk of streamReader(response)) {
processChunk(chunk);
}
- Cостоятельные сценарии (State Machines) Управление сложной бизнес-логикой с явным состоянием:
function* authSession() {
let user = null;
while (true) {
const action = yield { status: user ? 'loggedIn' : 'guest' };
switch (action.type) {
case 'LOGIN':
user = yield authenticate(action.credentials);
break;
case 'LOGOUT':
yield revokeToken(user.token);
user = null;
break;
case 'REFRESH':
user = yield renewSession(user.token);
break;
}
}
}
const session = authSession();
console.log(session.next().value.status); // 'guest'
session.next({ type: 'LOGIN', credentials: {...} });
console.log(session.next().value.status); // 'loggedIn'
Когда это оправдано?
Генераторы добавляют сложность. Рассмотрите их для:
- Длительных процессов с ручным управлением (загрузки файлов, WebSocket-сессии)
- Реализации пользовательских стримов
- Алгоритмов с необходимостью приостановки/продолжения
- Сложных конечных автоматов в UI/серверной логике
- Экзотических сценариев типа конкурентного программирования (Cooperative Multitasking)
Реальные нюансы
- Обработка ошибок: Используйте
gen.throw(error)
внутри драйверов для проброса исключений внутрь генератора в ближайшийtry/catch
. - Взаимодействие с Promise: Комбинируйте
yield
с промисами для асинхронных пауз. - Отладка: Стеки вызовов прерываются на
yield
, используйте source maps и явные точки останова. - Производительность: Не подходят для high-throughput микрозадач из-за накладных расходов на управление контекстом.
Заключение
Генераторы — мощный инструмент для сценариев, где требуется ручное управление потоком выполнения. Они не заменяют async/await
для линейного кода, но предлагают контроль над процессами там, где классические подходы усложняют архитектуру. Используйте их для обработки событий с состоянием, отменяемых операций, ленивых вычислений и сложных цепочек действий, особенно в библиотеках и инфраструктурном коде. Это не ежедневный инструмент, но козырь в рукаве при столкновении с необычными асинхронными вызовами.