Введение в генераторы

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

Для создания нового генератора мы используем новый синтаксис генератора функций function *. И когда вы запускаете такую ​​функцию в первый раз, она не выполняет свое тело сразу, а просто возвращает конкретный экземпляр генератора. Синтаксис создания выглядит как

После создания генератор переходит в исходное состояние. Ключевым методом работы с генератором является .next запуск генератора и продолжение работы до первого yield оператора. В этот момент генератор приостанавливает свое состояние и возвращает значение определения, которое стоит сразу после yield, во внешнюю область функции. В нашем примере мы просто создаем новый генератор и пытаемся получить первое значение yield:

В нашем случае firstNumber получает 10, а генератор приостанавливает выполнение до тех пор, пока мы не будем использовать numbers.next() в следующий раз. То же самое будет для следующих двух выполнений, в которых мы получим значения 20 и 30.

Но возникает вопрос: когда остановить выполнение? Есть ли какой-нибудь флаг, чтобы понять, что генератору больше нечего возвращать? да. Как вы можете видеть каждый раз, он возвращает объект типа {value: ..., done: ...}. И это ответ для нас. Последняя казнь будет выглядеть так:

Нам нужен флаг - свойство done возвращаемого объекта, наконец, его значение - true. Каждый следующий вызов .next будет вызывать ошибку и возвращать объект {value: undefined, done: true}.

Нет никакого обходного пути для сброса состояния созданного генератора. На данный момент единственный способ - просто создать новый экземпляр.

Двусторонний поток данных и выдача ошибок

Пожалуйста, рассмотрите следующий пример

При первом .next вызове мы возвращаем значение с текстом вопроса и сохраняем его во внешней области под переменной questionText. Во втором .next вызове мы передаем nameаргумент внутри. Этот аргумент возвращается как результат выражения yield и сохраняется внутри генератора под его переменной name, и мы можем управлять им при дальнейшем движении в коде и составлении возвращаемого значения Hello ${name}.

Но вы можете передать внутрь даже не значение, а ошибку. Вы можете сделать это с помощью метода .throw. Это вызовет исключение внутри генератора в строке с yield. Вы можете легко поймать его с помощью блока try{} catch{} внутри (пример ниже (1)). В пункте (2) он должен вызвать alert в примере.

Если вы не поймаете это в генераторе, исключение будет пузыриться в стеке за пределами генератора и начнет вылетать из вашего скрипта с unhandled exception, если также нет блоков catch вне генератора.

Итерация генераторов

Вы можете легко перебирать генераторы, потому что они являются повторяемыми объектами (метод .next говорит нам об этом).

Как вы уже заметили, структура for..of не возвращает нам последнее значение. Это особый случай, когда для итерации нам нужно использовать собственную реализацию с while или просто заменить return на yield для последнего случая.

Вы можете легко создавать бесконечные генераторы и использовать их в качестве генератора некоторых данных, а также while(true) { yield Math.random(); } в теле вашего генератора (но будьте осторожны с этим и всегда сохраняйте некоторую логику, чтобы остановить вашу итерацию).

Состав генераторов

Каждый генератор также может содержать дочерние генераторы. Для этого есть специальный синтаксис. Давайте посмотрим на примере:

Эта форма yield* может использоваться только внутри другого генератора и делегирует выполнение внутреннему генератору. Такая композиция работает, даже если внутренние генераторы производят бесконечную цепочку данных.

Побочные эффекты и простой асинхронный код с генераторами

Одна из основных областей использования генераторов - построение плоских асинхронных цепочек кода. Основная концепция этого:

  • yield должен возвращать обещания;
  • Генератор должен обслуживаться специальной внешней функцией, называемой менеджером. Этот менеджер вызывает generator.next и каждый раз получает новое обещание. После разрешения обещания значение возвращается генератору при следующем generator.next вызове.
  • Используя последнее полученное значение от генератора с {done: true}, менеджер обрабатывает его как значение результата всей логической цепочки.

Рассмотрим следующий пример:

Здесь asyncResult будет собирать значение результата после завершения всех итераций yield. Но давайте пошагово:

  • manager функция получает экземпляр генератора в качестве аргумента
  • Сразу после этого менеджер вызывает .next метод, чтобы получить currentOrganization обещание запроса, и если это не последний выход в генераторе, а next.done !== true менеджер ожидает разрешения обещания
  • После того, как обещание разрешено, он вызывает генератор и передает разрешенные данные в качестве второго аргумента. Эти данные будут сохранены как возвращаемое значение yield.
  • Генератор просыпается и продолжает выполнять свой код до следующего оператора yield.
  • Последний вызов вернет next.done === true менеджеру, который является флагом для записи next.value в качестве окончательного результата.

После слов

Такой подход помогает эффективно строить цепочки асинхронных запросов и помогает избежать ада обратных вызовов и наложения ада обещаний.

Есть несколько библиотек, которые используют такой подход под капотом. Один из них - co. Подробнее о реализациях можно прочитать на официальной странице.

Вторая библиотека более актуальна для тех, кто использует концептуальные решения redux single store. Это известно как redux-saga. Великолепное решение для управления побочными эффектами приложений, которым проще управлять, более эффективно выполнять, просто тестировать и лучше справляться со сбоями. Подробнее об этом вы можете узнать на официальной странице.