Материал, перевод которого мы сегодня публикуем, посвящён генераторам. А именно, тут мы поговорим о том, как они работают, и о том, как они, совместно с промисами, используются в недрах конструкции 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)
kemsky
23.07.2018 16:31+1Я бы не согласился по поводу применять, redux-saga позволяет легко писать тестируемый код на генераторах, и они оказываются проще, чем rxjs подход в ngrx.
apelsyn
23.07.2018 17:53Генераторы есть во многих языках, зачем от них отказываться?
Если речь идет о библиотеке co, то она была создана до того как async/await был добавлены в nodejs и реализовывала возможность писать код без callback hell-а.
После появления async/await необходимость в co отпала, самым популярным фреймворком, который популяризировал использование co был koajs v1.x. Новая версия koajs v2.x на использование генераторов выдает «warning» с «deprecated».
Суть этой истории сводится к тому что не нужно использовать генераторы не по назначению, а не к принципиальному отказу от использования генераторов.
force
23.07.2018 19:26+3На мой взгляд, генераторы, это самая странная фича ES6. Т.е. она логичная и полезная, yield иногда нужен, но иногда, это пару раз в год… Т.е. достаточно странный синтаксис (звёздочка в функции, всё время забываю куда её правильно ставить, ключевое слово yield или yield return?, соглашения на имена next и value) и ради того, чтобы возвращать итерируемые объекты.
При этом, на самом деле, все любители извращений тайно потирали руки и ждали реализации генераторов ради того, чтобы костылить на них эмуляцию async/await. И сразу после появления сделали пачку библиотек, которые это делают. Получается, возможность генераторов изначально все планировали использовать не по назначению.
Другими словами, добавили фичу, которую все используют для других целей. Может быть стоило сразу использовать async/await и забить на эти генераторы?mayorovp
23.07.2018 19:31+1К сожалению, генераторы до сих пор мощнее чем async/await. Например, с помощью async/await нельзя сделать асинхронный action в mobx без боли (а с помощью генератора — можно). Опять же, redux-saga использует именно генераторы.
Но в целом согласен. Кстати, в C++ посмотрели на это, да в Coroutines TS генераторы и асинхронные функции ввели одновременно, причем именно yield оказалось реализовано через await, а не как обычно.faiwer
23.07.2018 21:39Например, с помощью async/await нельзя сделать асинхронный action в mobx без боли (а с помощью генератора — можно)
А можно пример? Просто интересно, что там такое хитрое.
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; })
faiwer
24.07.2018 07:18Я правильно понимаю суть? В варианте с await если убрать
runInAction
, то послеthis.bar = ;
мы получим render, а потом ещё и послеthis.baz = ;
, т.к. рендер синхронный? А в случае*
mobx не обновляет при детекте изменений до тех пор, пока не получит через yield что-нибудь асинхронное, или генератор просто не кончится? Т.е. тут решает все проблемы ручная обработка каждого yield, позволяя сделать больше, чем написано в коде?mayorovp
24.07.2018 07:40+1Суть вы уловили правильно, но придумали переусложненный алгоритм. Функция flow просто оборачивает все вызовы next в runInAction.
morsic
23.07.2018 21:41Можно использовать для асинхронной итерации по списку значений которые приходят с задержками
github.com/tc39/proposal-async-iteration
RomanPokrovskij
24.07.2018 00:01Услышав высказывание «yeld pauses function execution» оно же «останавливает исполнение функции» — нужно остановить говорящего и настойчиво попросить быть корректным в формулировках. После точного «каждый yeld задает очередное состояние итератора возвращенного функцией» у автора бы не получилось статьи, но мы бы сэкономили время.
Gennadii_M
24.07.2018 08:50Я использовал генераторы в тестировании мобильных. У меня был объект, содержащий объекты с описанием устройств. Нужно было иметь возможность одновременно обращаться к конкретному девайсу по ключу и проходить по всем девайсам в цикле. Для этого я использовал генераторы. Функция возвращает объект, по которому можно бегать форычём и брать конкретный девайс по ключу. Плюс, если я хочу что-то подебажить, то мне не нужны все девайсы. Для этого был флаг в .env и по этому флагу генератор итерировался только по одному девайсу. Получилось очень удобно и достаточно просто. Не сказал бы что можно позволить себе не знать конструкции языка, доступные из коробки.
реализация хелпера
Nookie-Grey
24.07.2018 10:31Чёж пример async/await не показали, ради которого всё писалось, а какой-то устаревший co есть, как-будто старая статья...
bubandos
А что о них думать? О них знать нужно, и применять.
имхо, особенно полезны, когда нужно из разных источников собирать данные в одно целое)
да и конечные автоматы не теряют своей актуальности.
extempl
Что-то типа
Promise.all()
?bubandos
Когда вам нужно сделать цепочку асинхронных вызовов, в которой последующее действие зависит от результата предыдущего, да еще и с ветвлениями, Promise.all() не подойдет… с промисами тот еще кодхелл будет.
Может, конечно, такие задачи и не часто встречаются, но когда встречаются — генераторы очень быстро снимают головную боль)