JS: Синхронная асинхронность
Теория: Корутины
В Computer Science под генераторами понимается data producer, то есть сущность в языке, которая только выдает наружу данные, используя yield. При этом существует более общая концепция, которая называется coroutine или сопрограмма. В отличие от генераторов, она может не только генерировать данные, но так же может и потреблять их (data consumer). Самым удивительным в этой истории является то, что генераторы в js, по сути, являются корутинами, а использование их в качестве генераторов – это всего лишь один из возможных вариантов.
Сопрограмма — компонент программы, обобщающий понятие функции, который дополнительно поддерживает множество входных точек (а не одну, как функция), остановку и продолжение выполнения с сохранением определенного положения.
Главное, на что нужно обратить внимание, это появление выражения 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 автоматически итерирует по генератору, извлекая значение из промисов и передавая их дальше в next по цепочке. В целом, на этом можно было бы и остановиться, но для полной имитации синхронной работы хотелось бы поддержки со стороны try/catch. И генераторы дают возможность трансформировать ошибки в исключения.
Чтобы такой код заработал, необходимо в функции co отслеживать состояние rejected и использовать метод throw, который есть у нашего генератора. Ниже пример того, как это можно было бы сделать (без промисов):
Метод throw() возобновляет выполнение тела генератора, кидая внутри исключение, и возвращает объект со свойствами done и value.