С ростом популярности конструкции 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% на наши виртуальные сервера:
Автор: ru_vds