В Computer Science под генераторами понимается data producer
, то есть сущность в языке, которая только выдает наружу данные, используя yield
. При этом существует более общая концепция, которая называется coroutine
или сопрограмма. В отличие от генераторов, она может не только генерировать данные, но так же может и потреблять их (data consumer
). Самым удивительным в этой истории является то, что генераторы в js
, по сути, являются корутинами, а использование их в качестве генераторов – это всего лишь один из возможных вариантов.
Сопрограмма — компонент программы, обобщающий понятие функции, который дополнительно поддерживает множество входных точек (а не одну, как функция), остановку и продолжение выполнения с сохранением определенного положения.
const gen = function* () {
const a = yield 10;
const b = yield;
return a + b;
};
const coroutine = gen();
const result = coroutine.next(); // { value: 10, done: false }
coroutine.next(result.value + 1); // const a = 11
console.log(coroutine.next(15)); // const b = 15
// => { value: 26, done: true }
https://repl.it/@hexlet/js-sync-coroutines-gen
Главное, на что нужно обратить внимание, это появление выражения yield
справа от знака равно: const a = yield 10
.
Попробуем по шагам выполнить этот код:
- Создание корутины
const coroutine = gen();
- Вызов
next()
. Первый вызов приводит к тому, что наружу возвращается{ value: 10, done: false }
, так как внутри мы оказываемся в точкеyield 10
. - Вызов
next(result.value + 1)
. Выражениеresult.value + 1
равно11
, поэтому в итоге происходит вызовnext(11)
. Внутри корутины мы находимся в этой позицииconst a = yield
. Аргумент, переданный вnext
, оказывается записанным в константуa
внутри корутины и код продолжает выполняться до следующего вызоваyield
, на котором корутина останавливается, и управление возвращается наружу. - Дальнейший вызов
next(15)
приводит к тому, что константаb
становится равна15
, а наружу возвращается{ value: 26, done: true }
.
Если обобщить, то yield <что-то>
производит данные наружу, const a = yield
потребляет данные, а const a = yield <что-то>
производит и потребляет в два шага.
Теперь, используя немного магии, мы можем создать обертку над генераторами для работы с асинхронным кодом. Функция co
будет такой оберткой. Ниже пример как она используется:
co(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
const c = yield Promise.resolve(3);
console.log([a, b, c]); // => [1, 2, 3]
});
Идея в том, что функция co
автоматически итерирует по генератору, извлекая значение из промисов и передавая их дальше в next
по цепочке. В целом, на этом можно было бы и остановиться, но для полной имитации синхронной работы хотелось бы поддержки со стороны try/catch
. И генераторы дают возможность трансформировать ошибки в исключения.
co(function* () {
const a = yield Promise.resolve(1);
try {
const b = yield Promise.reject(new Error('Boom'));
} catch (e) {
console.log(e.message); // => 'Boom'
}
});
Чтобы такой код заработал, необходимо в функции co
отслеживать состояние rejected
и использовать метод throw
, который есть у нашего генератора. Ниже пример того, как это можно было бы сделать (без промисов):
const gen = function* () {
try {
const a = yield;
yield new Error('Boom');
} catch (e) {
console.log(e.message);
}
console.log('after Boom');
};
const coroutine = gen();
coroutine.next();
const result = coroutine.next();
coroutine.throw(result.value); // => { value: undefined, done: true }
// Boom
// After Boom
Метод throw()
возобновляет выполнение тела генератора, кидая внутри исключение, и возвращает объект со свойствами done и value.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты