С ростом популярности конструкции async/await растёт и интерес к её внутренним механизмам. Порывшись в интернете, несложно выяснить, что в основе async/await лежат широко известные промисы, и генераторы, которые пользуются куда меньшей известностью и популярностью.



Материал, перевод которого мы сегодня публикуем, посвящён генераторам. А именно, тут мы поговорим о том, как они работают, и о том, как они, совместно с промисами, используются в недрах конструкции async/await. Автор этой статьи говорит, что генераторы, ради их практического применения, осваивать необязательно. Кроме того, он отмечает, что он рассчитывает на то, что читатель немного разбирается в промисах.

Итераторы и генераторы


В JavaScript, начиная с выхода стандарта ES6, появилось несколько новых возможностей, которые направлены на упрощение работы с асинхронными потоками данных и коллекциями. В эту категорию попадают итераторы и генераторы.

Примечательной возможностью итераторов является то, что они предоставляют средства для доступа к элементам коллекций по одному за раз, и при этом позволяют отслеживать идентификатор текущего элемента.

function makeIterator(array) {
  var nextIndex = 0;
  console.log("nextIndex =>", nextIndex);

  return {
    next: function() {
      return nextIndex < array.length
        ? { value: array[nextIndex++], done: false }
        : { done: true };
    }
  };
}

var it = makeIterator(["simple", "iterator"]);

console.log(it.next()); // {value: 'simple, done: false}
console.log(it.next()); // {value: 'iterator, done: false}
console.log(it.next()); // {done: true}

Выше мы передаём функции makeIterator() небольшой массив, содержащий пару элементов, после чего проходимся по нему с помощью итератора, вызывая метод it.next(). Обратите внимание на комментарии, демонстрирующие получаемые с помощью итератора результаты.

Теперь поговорим о генераторах. Генераторы — это функции, которые работают как фабрики итераторов. Рассмотрим простой пример, а затем поговорим о двух механизмах, имеющих отношение к генераторам.

function* sample() {
  yield "simple";
  yield "generator";
}

var it = sample();

console.log(it.next()); // {value: 'simple, done: false}
console.log(it.next()); // {value: 'generator, done: false}
console.log(it.next()); // {value: undefined, done: true}

Обратите внимание на звёздочку в объявлении функции. Это указывает на то, что данная функция является генератором. Кроме того, взгляните на ключевое слово yield. Оно приостанавливает выполнение функции и возвращает некое значение. Собственно, эти две особенности и являются теми самыми двумя механизмами, о которых мы говорили выше:

  • Функция-генератор — это функция, объявленная с использованием звёздочки около ключевого слова function или около имени функции.
  • Итератор генератора создаётся, когда вызывают функцию-генератор.

В общем-то, вышеописанный пример демонстрирует работу фабричной функции, генерирующей итераторы.

Теперь, когда мы разобрались в основах, поговорим о более интересных вещах. Итераторы и генераторы могут обмениваться данными в двух направлениях. А именно, генераторы, с помощью ключевого слова yield, могут возвращать значения итераторам, однако и итераторы могут отправлять данные генераторам, используя метод iterator.next('someValue'). Вот как это выглядит.

function* favBeer() {
  const reply = yield "What is your favorite type of beer?";
  console.log(reply);
  if (reply !== "ipa") return "No soup for you!";
  return "OK, soup.";
}

{
  const it = favBeer();
  const q = it.next().value; // Итератор задаёт вопрос
  console.log(q);
  const a = it.next("lager").value; // Получен ответ на вопрос
  console.log(a);
}

// What is your favorite beer?
// lager
// No soup for you!

{
  const it = favBeer();
  const q = it.next().value; // Итератор задаёт вопрос
  console.log(q);
  const a = it.next("ipa").value; // получен ответ на вопрос
  console.log(a);
}

// What is your favorite been?
// ipa
// OK, soup.

Генераторы и промисы


Теперь мы можем поговорить о том, как генераторы и промисы формируют базу конструкции async/await. Представьте, что вместо того, чтобы возвращать, с помощью ключевого слова yield, некие значения, генератор возвращает промисы. При таком раскладе генератор можно обернуть в функцию, которая будет ожидать разрешения промиса и возвращать значение промиса генератору в методе .next(), как было показано в предыдущем примере. Существует популярная библиотека, co, которая выполняет именно такие действия. Выглядит это так:

co(function* doStuff(){
  var result - yield someAsyncMethod();
  var another = yield anotherAsyncFunction();
});

Итоги


По мнению автора этого материала JS-разработчикам нужно знать о том, как работают генераторы, лишь для того, чтобы понимать особенности внутреннего устройства конструкции async/await. А вот использовать их непосредственно в собственном коде не стоит. Генераторы вводят в JavaScript возможность приостанавливать выполнение функции и возвращаться к ней когда (и если) разработчик сочтёт это необходимым. До сих пор мы, работая с JS-функциями, ожидали, что они, будучи вызванными, просто выполняются от начала до конца. Возможность их приостанавливать — это уже что-то новое, но этот функционал удобно реализован в конструкции async/await.

С этим мнением, конечно, можно и поспорить. Например, один из аргументов в пользу генераторов, сводится к тому, что знание того, как они работают, полезно для отладки кода с async/await, так как внутри этой конструкции скрываются генераторы. Однако автор материала полагает, что это, всё же, нечто иное, нежели использование генераторов в собственном коде.

Уважаемые читатели! Что вы думаете о генераторах? Может быть, вы знаете какие-то варианты их использования, которые оправдывают их непосредственное применение в коде JS-проектов?

Царский промо-код для скидки в 10% на наши виртуальные сервера:

Комментарии (15)


  1. bubandos
    23.07.2018 15:46

    А что о них думать? О них знать нужно, и применять.
    имхо, особенно полезны, когда нужно из разных источников собирать данные в одно целое)
    да и конечные автоматы не теряют своей актуальности.


    1. extempl
      23.07.2018 19:33

      особенно полезны, когда нужно из разных источников собирать данные в одно целое)

      Что-то типа Promise.all()?


      1. bubandos
        24.07.2018 14:33

        Когда вам нужно сделать цепочку асинхронных вызовов, в которой последующее действие зависит от результата предыдущего, да еще и с ветвлениями, Promise.all() не подойдет… с промисами тот еще кодхелл будет.
        Может, конечно, такие задачи и не часто встречаются, но когда встречаются — генераторы очень быстро снимают головную боль)


  1. kemsky
    23.07.2018 16:31
    +1

    Я бы не согласился по поводу применять, redux-saga позволяет легко писать тестируемый код на генераторах, и они оказываются проще, чем rxjs подход в ngrx.


  1. apelsyn
    23.07.2018 17:53

    Генераторы есть во многих языках, зачем от них отказываться?

    Если речь идет о библиотеке co, то она была создана до того как async/await был добавлены в nodejs и реализовывала возможность писать код без callback hell-а.

    После появления async/await необходимость в co отпала, самым популярным фреймворком, который популяризировал использование co был koajs v1.x. Новая версия koajs v2.x на использование генераторов выдает «warning» с «deprecated».

    Суть этой истории сводится к тому что не нужно использовать генераторы не по назначению, а не к принципиальному отказу от использования генераторов.


  1. force
    23.07.2018 19:26
    +3

    На мой взгляд, генераторы, это самая странная фича ES6. Т.е. она логичная и полезная, yield иногда нужен, но иногда, это пару раз в год… Т.е. достаточно странный синтаксис (звёздочка в функции, всё время забываю куда её правильно ставить, ключевое слово yield или yield return?, соглашения на имена next и value) и ради того, чтобы возвращать итерируемые объекты.
    При этом, на самом деле, все любители извращений тайно потирали руки и ждали реализации генераторов ради того, чтобы костылить на них эмуляцию async/await. И сразу после появления сделали пачку библиотек, которые это делают. Получается, возможность генераторов изначально все планировали использовать не по назначению.

    Другими словами, добавили фичу, которую все используют для других целей. Может быть стоило сразу использовать async/await и забить на эти генераторы?


    1. mayorovp
      23.07.2018 19:31
      +1

      К сожалению, генераторы до сих пор мощнее чем async/await. Например, с помощью async/await нельзя сделать асинхронный action в mobx без боли (а с помощью генератора — можно). Опять же, redux-saga использует именно генераторы.

      Но в целом согласен. Кстати, в C++ посмотрели на это, да в Coroutines TS генераторы и асинхронные функции ввели одновременно, причем именно yield оказалось реализовано через await, а не как обычно.


      1. faiwer
        23.07.2018 21:39

        Например, с помощью async/await нельзя сделать асинхронный action в mobx без боли (а с помощью генератора — можно)

        А можно пример? Просто интересно, что там такое хитрое.


        1. mayorovp
          23.07.2018 22:34
          -1

          Вот так это делается с async/await:


          @action
          async foo() {
              const result = await fetch(...);
              const data = await result.json();
              runInAction(() => {
                  // без runInAction будет лишний рендер, потому что mobx не может отловить асинхронные продолжения и обернуть их в транзакцию
                  this.bar = data.bar;
                  this.baz = data.baz;
              });
          }

          А вот так это делается с генератором:


          foo = flow(function * () {
              const result = yield fetch(...);
              const data = yield result.json();
          
              // поскольку mobx сама управляет вызовами продолжений нет никаких проблем обернуть каждое в транзакцию автоматически, runInAction не нужен
              this.bar = data.bar;
              this.baz = data.baz;
          })


          1. faiwer
            24.07.2018 07:18

            Я правильно понимаю суть? В варианте с await если убрать runInAction, то после this.bar = ; мы получим render, а потом ещё и после this.baz = ;, т.к. рендер синхронный? А в случае * mobx не обновляет при детекте изменений до тех пор, пока не получит через yield что-нибудь асинхронное, или генератор просто не кончится? Т.е. тут решает все проблемы ручная обработка каждого yield, позволяя сделать больше, чем написано в коде?


            1. mayorovp
              24.07.2018 07:40
              +1

              Суть вы уловили правильно, но придумали переусложненный алгоритм. Функция flow просто оборачивает все вызовы next в runInAction.


  1. morsic
    23.07.2018 21:41

    Можно использовать для асинхронной итерации по списку значений которые приходят с задержками
    github.com/tc39/proposal-async-iteration


  1. RomanPokrovskij
    24.07.2018 00:01

    Услышав высказывание «yeld pauses function execution» оно же «останавливает исполнение функции» — нужно остановить говорящего и настойчиво попросить быть корректным в формулировках. После точного «каждый yeld задает очередное состояние итератора возвращенного функцией» у автора бы не получилось статьи, но мы бы сэкономили время.


  1. Gennadii_M
    24.07.2018 08:50

    Я использовал генераторы в тестировании мобильных. У меня был объект, содержащий объекты с описанием устройств. Нужно было иметь возможность одновременно обращаться к конкретному девайсу по ключу и проходить по всем девайсам в цикле. Для этого я использовал генераторы. Функция возвращает объект, по которому можно бегать форычём и брать конкретный девайс по ключу. Плюс, если я хочу что-то подебажить, то мне не нужны все девайсы. Для этого был флаг в .env и по этому флагу генератор итерировался только по одному девайсу. Получилось очень удобно и достаточно просто. Не сказал бы что можно позволить себе не знать конструкции языка, доступные из коробки.
    реализация хелпера


  1. Nookie-Grey
    24.07.2018 10:31

    Чёж пример async/await не показали, ради которого всё писалось, а какой-то устаревший co есть, как-будто старая статья...