Вы слышали это раньше, возможно, даже сказали это (я знаю, что слышал): «Async / Await? Да, это просто синтаксический сахар, построенный на основе генераторов ". Вы, наверное, похвалили себя за то, что показались таким умным (я знаю, что так и было), но вы действительно поняли это утверждение? Я знаю, что не знал (и знаю, что вы устали от всего этого «я знаю»).

Давайте подробно рассмотрим, что означает, когда мы говорим, что Async / Await построен на основе генераторов.

Я предполагаю, что для целей этой статьи вы знакомы с промисами. Если вы этого не сделаете, это объект, предоставленный нам спецификацией JavaScript es2015, который позволяет нам обрабатывать асинхронные действия без использования устаревших функций обратного вызова. Перефразируя великого Кайла Симпсона, подумайте об обещании как о квитанции от вашего бариста. Вы подходите к прилавку, заказываете тройной латте, но не сразу получаете кофе - вам нужно подождать. Бариста дает вам обещание (в данном случае квитанцию), что вы получите свой кофе, когда он будет готов, или, если у них кончатся зерна во время второй порции, и они не смогут приготовить ваш напиток, вы получите извинения. Если они хорошие. В любом случае обещание представляет собой некую будущую ценность: если действие выполнено успешно, вы получите свой кофе (данные), если нет, вы получите свои извинения (ошибка).

Кстати: однажды я слышал, как концепции программирования оцениваются по количеству блогов, то есть сколько блогов вам придется прочитать, чтобы по-настоящему понять это. Для меня Promises были, наверное, 50 (и все еще подсчитываются) по шкале подсчета блогов, и добавляли книгу или две. Но здесь не нужно разбираться во всех мелочах.

Что вам действительно нужно знать, так это то, что когда Promise, наконец, получает свое значение или свою ошибку, он вызывает некоторые функции, которые вы ему дали. У обещаний есть .then method для регистрации произвольного количества успешных обратных вызовов и .catch method для принятия обратного вызова ошибки.

fetchUsers()
  .then(res => {/* this function is called when the promise
                  ‘resolves’ (or your value comes back from whatever
                  api you hit, could have been the file system, 
                  twitter's api, whereever */})
 .then(res => {/* this function is called after the first function
                  you registered with .then. Again, you can register
                  as many as you want and they’ll be called in
                  order*/})
 .catch(error => {/* this function is called if there is an error at
                  anypoint in your promise chain. It’s an error
                  catch all. You can log out you error, call some
                  error reporting service, whatever your app calls
                  for */});

Генераторы

Генераторы - это особый тип функций. Это первая функция в JavaScript, которая не следует семантике «выполнение до завершения». Что это значит? Если вы определяете функцию doSomeStuff:

function doSomeStuff() {
  let num = 1;
  num += 2;
  console.log(‘what am I doing’);
  num += 3;
}

Эта функция начнет выполнение первой строки своей функции и не остановится, пока она:

  1. Находит заявление о возврате
  2. Достигает последней строки и неявно возвращает undefined
  3. Выдает ошибку.

Генераторы ведут себя по-разному, их можно приостановить (приостановить) в середине выполнения. Давайте взглянем:

Первое, на что следует обратить внимание, - это маленькая звездочка после ключевого слова function, которая обозначает это как функцию генератора. В остальном он очень похож на любую старую функцию, за исключением yield keyword. yield действует как оператор return, с той разницей, что вместо того, чтобы навсегда остановить выполнение этого тела функции (как обычный returnwould), yield приостанавливает выполнение функции. Он также выбрасывает любое значение справа от того, что называется функцией. Итак, теперь давайте посмотрим, как вы вызываете генератор и что он возвращает:

const iteratorObject = doSomeStuff(); // { next: function() {...} }

Вы догадались по названию. Когда вы вызываете функцию генератора doSomeStuff, вы получаете объект-итератор, который также является новой функцией языка. Мы не будем здесь подробно останавливаться на итераторах, но вам нужно знать, что объект итератора содержит метод next:

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

Поговорим об этом. В строке 1 мы объявляем нашу функцию генератора. В строке 7 мы вызываем эту функцию, сохраняя ее возвращаемое значение (объект-итератор) в нашей константе iteratorObject. На линии 9 мы звоним next. Это выводит наш поток выполнения из глобальной области видимости в тело функции doSomeStuff. Так уж получилось, что в первой строке тела функции мы попали в оператор yield. Это возвращает значение 1 за пределы функции обратно в наш основной глобальный контекст выполнения. Обратите внимание, что возвращаемое значение next - это объект со свойством value и свойством done. Мы продолжаем выполнение таким образом, пока не исчерпаем функцию генератора. Обратите внимание, что самый последний вызов возвращает undefined как значение, а для done установлено значение true. Помните это (ооооо, предзнаменование).

Мы только что видели, как работает базовая функция генератора. Еще одна вещь, которую следует отметить в next, заключается в том, что он не только возвращает значения, вы можете использовать его для вставки значений в генератор, например:

Обратите внимание, что в строке 8 мы впервые вызываем next, которая вводит нас в тело функции calculateSomething. Устанавливаем переменную num и временно храним в ней 1. Затем мы объявляем константу anotherNum, которая будет хранить результат того, что разрешает выражение справа от знака равенства. А что здесь справа? Ага, yield заявление. Поэтому, прежде чем что-либо получит возможность сохраниться в anotherNum, мы возвращаемся в наш основной контекст выполнения. Однако на этот раз мы вызываем next со значением, которое нужно отправить обратно в генератор, в данном случае это результат сложения firstNum.value и 98. Наконец, мы сохраняем возвращенный результат нашего вычисления в anotherNum и нажимаем return оператор (генератору не нужен оператор возврата, но он у нас есть). Мы можем сказать, что генератор готов, потому что значение done истинно (предсказание продолжается…).

Надеюсь, вы заметили здесь проблему. Чтобы вызвать эту функцию генератора и запустить ее до завершения (помните: нам не нужно запускать генератор до тех пор, пока он не будет исчерпан, мы можем остановиться в любой момент), мы должны заранее знать, сколько операторов yield находится в функции. Возможно, здесь это не имеет большого значения, но мы увидим, когда начнем разбираться в более практических приложениях, насколько это на самом деле ограничение. Однако мы можем обойти это. Мы можем определить утилиту, которая будет обрабатывать вызовы нашего генератора до тех пор, пока он не будет исчерпан. Опять же, я знаю, что это не кажется важным для такой тривиальной функции, но это создает основу для того, как такие инструменты, как Babel, даже сами браузеры, обрабатывают Async / Await. Чтобы понять это, давайте посмотрим на асинхронный код:

Что тут происходит? Сначала мы импортируем axios, чтобы сделать запрос XHR. Вы можете использовать fetch или любую другую библиотеку, которая вам нравится. Наша функция axios здесь возвращает нам обещание, поскольку запрос XHR выполняется асинхронно. Затем мы объявляем функцию генератора githubSentence. Эта функция использует ответы, возвращаемые API Github, для завершения предложения о пользователе, которого вы передаете. Хотя это немного надумано, вы можете представить себе раздел «о себе» для пользователя и, возможно, включающий информацию таким же образом, как этот. Мы дважды нажимаем Github: один раз, чтобы получить общую информацию, чтобы получить доступ к подсчету public_repos пользователя, и еще раз, чтобы узнать количество пользователей, на которых они подписаны.

Что нам дает генераторы? Вы только посмотрите на код, он выглядит чертовски синхронно. Сравните приведенный выше код с этим кодом и решите, что выглядит чище:

Является ли приведенный выше код несостоятельным? Нет, и бывают случаи, когда с хорошим ‘оле .then может быть легче иметь дело. Но цель этой статьи не в этом, поэтому еще раз, вот наша версия, созданная на основе генератора:

Возможно, чище и проще для понимания. Так как бы мы это назвали? Что ж, вот один способ:

Что тут происходит? Мы объявляем переменную sentence, которая будет содержать ссылку на объект Promise. Мы создаем конструктор обещаний и погружаемся в его определение. В строке 4 мы инициализируем нашу функцию генератора, а в строке 5, вызывая next, мы запускаем ее. Теперь наш поток выполнения находится внутри githubSentence. На этот раз мы даем обещание. Итак, если мы посмотрим на строку 6 выше, value это объект Promise, а не просто примитивное значение, как мы видели в наших предыдущих примерах, когда выдавали числа. Мы разрешаем значение этого обещания, заключая его в Promise.resolve. Зачем это делать? Почему бы просто не позвонить value.then() напрямую? Что, если полученное значение не было обещанием, как мы ожидали? Тогда мы получим ошибку при попытке вызвать then для некоторого значения, не имеющего then метода. Обернув value в Promise.resolve, мы гарантируем, что сможем согласованно взаимодействовать с любым значением, возвращаемым нашим генератором. Нам не нужно беспокоиться, обещание это или нет.

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

Итак, внутри функции генератора результат нашего первого вызова API теперь сохраняется в переменной githubResponse, и выполнение продолжается. В строке 10 мы добавляем еще немного к нашему предложению, а в строке 14 из githubSentence мы объявляем константу following. following сохранит результат того, что оценивает выражение с правой стороны. В этом случае, о чудо, есть еще одно yield утверждение !! Мы бросаем любое выражение справа от оператора yield обратно тому, кто его вызвал. В этом случае мы вызвали его из строки 8 в нашем примере вызова выше. И снова мы идем через те же песни и танцы. itr в строке 8 содержит ссылку на обещание, выданное githubSentence, в строке 9 мы сохраняем ссылку на поле value в объекте итератора, разрешаем его значение и снова запускаем все это с помощью еще одного вложенного then. Пройдите оставшуюся часть выполнения самостоятельно, чтобы увидеть, понимаете ли вы, как мы приходим к нашему окончательному значению.

На что стоит обратить внимание: посмотрите на вложение выше - мы вызываем then один раз в Promise, на которое ссылается value в строке 7, мы делаем то же самое в строке 10, и вы можете себе представить, для каждого yield оператора в функции нам пришлось бы продолжайте выполнять обещания и звонить then. Мы можем попасть на очень глубокие уровни вложенности, и очень больно все это отслеживать. Я думаю, что всякий раз, когда я вижу появление такого рода шаблонов, здесь может быть рекурсивное решение, которое может спасти нас от всего этого ручного вложения, когда мы повторяем одни и те же операции снова и снова. Если вы это уже видели и поняли то же самое, хорошо, давайте будем друзьями-ботаниками по функциональному программированию. Если вы этого не видели, мы все равно можем быть друзьями, но давайте посмотрим, как мы могли бы определить функцию, которая запускает за нас наш генератор и обрабатывает все эти беспорядочные вложения и повторяющиеся операции.

Стреляй, подожди ... чтобы рекурсивная функция не просто вызывала себя снова и снова 1000000 раз и взрывала стек, должен быть базовый случай, когда мы можем сказать: «Хватит, стоп !!!», но что мы можем использовать в качестве базовый случай здесь? Вы говорите, значение done на объекте итератора? Да это прекрасно! Когда done истинно, мы знаем, что исчерпали объект итератора, и нам не нужно продолжать работу. Видите ли, все то, что было раньше, наконец-то окупилось!

Вот он, давай поговорим об этом. Итак, если вы уже являетесь одним из моих приятелей по FP, вы можете видеть, что мы определяем функцию более высокого порядка (функцию, которая принимает другую функцию в качестве аргумента), называемую runner. Требуется fn и предупреждение о спойлере, мы собираемся передать ему генератор, и он принимает аргумент для вызова этой функции с arg. Затем мы объявляем две переменные, itr и value, как в предыдущем примере. Почему мы должны объявить их выше в строках 4 и 5? Поскольку ключевые слова const и let имеют блочную область видимости, то есть, если мы объявим их в строках 9 и 10, мы не сможем получить к ним доступ, начиная со строк 16.

Возвращаемое значение нашей runner функции - это Promise, которое мы создаем с нуля с помощью конструктора Promise. Внутри его определения мы объявляем функцию step, это функция, которая будет выполнять нашу рекурсию за нас, автоматически обрабатывая все эти повторяющиеся операции. step принимает 2 аргумента: key и arg. key будет методом, который мы будем использовать для вызова нашего генератора, и есть 2 варианта. Мы можем вызвать генератор с next, как мы это делали все это время, но мы также можем вызвать его с throw, которого мы еще не видели. Throw вызовет любую обработку ошибок внутри функции генератора. Итак, если бы мы заключили тело функции генератора в try/catch, вызов throw в нашем объекте итератора вызвал бы обработчик catch в нашем генераторе.

Строка 9 запускает наш try/catch блок. Мы вызываем генератор, который может запускать действие, которое может привести к ошибке (в нашем случае мы вызываем API Github, который может быть недоступен, может произойти тайм-аут, может возникнуть любое количество проблем). Если есть ошибка, мы вызываем reject с ошибкой, в противном случае мы продолжаем. Строка 16 - это базовый случай нашей рекурсии, мы проверяем, готов ли генератор. Если это так, мы выполнили всю необходимую работу и вызываем resolve с нашим значением. Если он не выполнил свою работу, мы разрешаем свойство value объекта итератора (точно так же, как мы это делали, когда писали все это вручную в предыдущей сущности), и снова вызываем нашу функцию step (это рекурсивный part) либо с помощью 'next' и текущего значения, либо мы вызываем его с помощью 'throw', чтобы указать, что произошла ошибка и какой-либо объект ошибки был возвращен.

Вот и все! Чтобы вызвать нашу githubSentence функцию с нашей новой функцией-раннером, мы делаем это следующим образом:

const sentence = runner(githubSentence, "shaneu");

Помните, что возвращаемое значение - это обещание, поэтому мы можем получить доступ к нашему завершенному предложению, вызвав then:

sentence.then(result => /* the returned sentence */)

Что, если бы я показал вам это:

Похоже, что если бы мы добавили ключевое слово async прямо перед function и заменили эти yields операторами await, почему бы нам не искать функцию Async / Await! И вы абсолютно правы. Babel преобразует вашу асинхронную функцию в генератор и выполнит ее с помощью функции runner. Давайте посмотрим, как наша финальная версия находится рядом с версией генератора, и как мы назовем обе:

Вот и все. Мы только что создали Async / Await с нуля с помощью генераторов. Надеюсь, теперь вы видите, что на самом деле это просто синтаксический сахар (большая чашка сахара с горкой) вместо того, чтобы делать то же самое с функциями генератора.

Итак, нужно ли знать, как переписать Async / Await с нуля, чтобы использовать эту функцию? Точно нет. Я твердо верю, что понимаю инструменты и абстракции, которые мы используем, когда можем. Не знаю, как вы, но мое понимание всех вышеперечисленных концепций стало намного сильнее, когда я нырнул в эту кроличью нору.

В заключение, функция Async / Await предоставляет нам средства для написания асинхронного кода таким образом, чтобы он выглядел синхронным. Мы можем вызвать функцию, которая выполняет какое-либо асинхронное действие, и с помощью ключевого слова await мы можем приостановить выполнение этой функции до тех пор, пока не получим наш результат. Мы увидели, как именно это достигается, как на самом деле можно приостановить выполнение функции (что ранее было невозможно), если эта функция является генератором.

Я надеюсь, что вы стали лучше понимать эту замечательную функцию и можете продолжать ее использовать, возможно, с большей уверенностью, чем раньше. И кто знает, возможно, вы сможете ответить на этот сложный вопрос интервью чуть более детальным ответом, чем «Async / Await? Да, это просто синтаксический сахар, построенный на основе генераторов ".