Что такое Генераторы в JavaScript
На прошлой недели в этом блоге, я опубликовал статью о Итераторах (Как перестать бояться и полюбить Итераторы). Итераторы часто используются вместе генераторами. Поэтому очевидным продолжением того поста, должна быть статья о Генераторах в Javascript.
При чтение рекомендуется весь код из статьи продублировать в каком-нибудь онлайн редакторе типа codesandbox.io.
Спонсор поста Реджери — удобный регистратор доменов и современный хостинг.
Используя многолетний опыт работы в доменной индустрии, компания Regery зарекомендовала себя как ведущий поставщик услуг регистрации и управления доменами. Если вы покупаете домены Regery, то можете в пределах площадки размещать их на хостинге.
Regery позволяет небольшим предприятиям и организациям создавать онлайн-присутствие без необходимости в обширных технических знаниях или ресурсах.
Что такое Генераторы?
Обычная функция, такая как эта, не может быть остановлена в процессе выполнения, пока она не завершит свою задачу, то есть будет выполнена ее последняя строка. Это называется моделью run-to-completion.
function normalFunc() { console.log('I') console.log('cannot') console.log('be') console.log('stopped.') }
Единственный способ выйти из normalFunc — это выполнить ее до конца, использовать return или столкнуться с ошибкой. Если вы снова вызовете функцию, она снова начнет выполнение сверху.
Напротив, генератор — это функция, которая может остановиться на полпути, а затем продолжить с того места, где остановилась.
Вот некоторые другие общие определения генераторов:
- Генераторы — это особый класс функций, которые упрощают задачу написания итераторов.
- Генераторы — это способ генерации серии значений или запуска ряда операций. Этот процесс может быть конечным или продолжаться вечно.
Пример генератора:
const generatorFunction = function* () {};
Переменной generatorFunction присваивается функция генератора. Функции генератора в ES6 обозначаются с использованием синтаксиса function*.
Вызов функции генератора должен возвращать объект итератора iterator вида:
{ value: Any, done: true|false }
const generatorFunction = function* () { // Это не выполняется. console.log('a'); }; console.log(1); const iterator = generatorFunction(); console.log(2); // 1 // 2
Для выполнения тела генератора используется метод next():
const generatorFunction = function* () { console.log('a'); }; console.log(1); const iterator = generatorFunction(); console.log(2); iterator.next(); console.log(3); // 1 // 2 // a // 3
Метод next() как было сказано выше возвращает объект, который указывает на ход итерации:
const generatorFunction = function* () {}; const iterator = generatorFunction(); console.log(iterator.next()); // Object {value: undefined, done: true}
Свойство done со значением true указывает, что тело генератора было пройдено и завершено.
Если нужно вернуть значение из генератора нужно использовать ключевое слово yield. yield приостанавливает выполнение генератора и возвращает управление итератору.
const generatorFunction = function* () { yield; }; const iterator = generatorFunction(); console.log(iterator.next()); console.log(iterator.next()); // Object {value: undefined, done: false} // Object {value: undefined, done: true}
При приостановке генератор не блокирует очередь событий:
const generatorFunction = function* () { var i = 0; while (true) { yield i++; } }; const iterator = generatorFunction(); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); // Object {value: 0, done: false} // Object {value: 1, done: false} // Object {value: 2, done: false} // Object {value: 3, done: false} // Object {value: 4, done: false} // Object {value: 5, done: false}
Передача значения из Генератора
const generatorFunction = function* () { yield 'foo'; }; const iterator = generatorFunction(); console.log(iterator.next()); console.log(iterator.next()); // Object {value: "foo", done: false} // Object {value: undefined, done: true}
Может быть передан любой тип данных, включая functions, numbers, arrays и objects.
Так же когда генератор подходит к завершению, можно указать что бы return вернул последнее значение.
const generatorFunction = function* () { yield 'foo'; return 'bar'; }; const iterator = generatorFunction(); console.log(iterator.next()); console.log(iterator.next()); // Object {value: "foo", done: false} // Object {value: "bar", done: true}
Передача значения в Генератор
Ключевое слово yield может получить значение обратно от итератора:
const generatorFunction = function* () { console.log(yield); }; const iterator = generatorFunction(); iterator.next('foo'); iterator.next('bar'); // bar
yield не получает первое значение «foo», потому оно не учитывается.
Итерация с использованием for … of
Объект итератора, возвращаемый генератором, соответствует «итеративному» протоколу. Таким образом, вы можете использовать оператор for … of для обхода генератора.
const generatorFunction = function* () { yield 1; yield 2; yield 3; return 4; }; const iterator = generatorFunction(); for (const value of iterator) { console.log(value); } // Should give us: // 1 // 2 // 3
- Итерация будет продолжаться до тех пор, пока свойство done будет имеет значение false.
- Цикл for..of нельзя использовать в тех случаях, когда вам нужно передать значения шагам генератора.
- Цикл for..of выбросит возвращаемое значение return.
Но, если вы хотите превратить эту итерацию в массив, вам не нужно использовать for-of; вместо этого вы можете использовать «spread».
const iterator = generatorFunction(); const fromIterable = [...iterator]; console.log(fromIterable); // [1, 2, 3]
Универсальность итераций в JavaScript делает этот шаблон очень мощным. На самом деле, много конструкций в JavaScript либо принимают итерируемые объекты — iterables, либо сами по себе итерируемые — iterables! Например, массивы определяются как итерируемые.
Если вы хотите, вы можете использовать «spread» как список параметров для функции. Такая конструкция называется rest
someSpreadable(...iterable);
Не только массивы могут использовать spread; В общем случае все iterables могут применять оператор spread.
С помощью генераторов вы можете не только вывести (yield) одно значение (типа yield X), но вы также можете вывести другой yield . И так, вы можете переписать вышеупомянутую функцию generatorFunction, чтобы вывести (yield) отдельные 1, 2 и 3 из массива. Просто обязательно добавьте * сразу после ключевого слова yield.
function * generatorFunction() { yield * [1, 2, 3]; }
Для чего нужны Генераторы?
В JavaScript операции ввода-вывода обычно выполняются как асинхронные операции, требующие обратного вызова. Для иллюстрации я собираюсь использовать тестовый сервис foo:
const foo = (name, callback) => { setTimeout(() => { callback(name); }, 100); };
Многочисленные асинхронные операции одна за другой создают вложенность, которую трудно читать.
const foo = (name, callback) => { setTimeout(() => { callback(name); }, 100); }; foo('a', (a) => { foo('b', (b) => { foo('c', (c) => { console.log(a, b, c); }); }); }); // a // b // c
Существует несколько способов решения этой проблемы, например, использование promises или генераторы. Используя генераторы, приведенный выше код можно переписать так. Но что бы его запустить нужно еще кое что:
(function* () { const a = yield curry(foo, 'a'); const b = yield curry(foo, 'b'); const c = yield curry(foo, 'c'); console.log(a, b, c); });
Чтобы запустить генератор, нам нужен контроллер. Контроллер должен выполнить асинхронные запросы и вернуть результат обратно.
/** * Инициирует генератор и выполняет итерацию по каждой * предоставленной функции через оператора yield. * * @param {Function} */ const controller = (generator) => { const iterator = generator(); const advancer = (response) => { // Advance the iterator using the response of an asynchronous callback. const state = iterator.next(response); if (!state.done) { // Make the asynchronous function call the advancer. state.value(advancer); } } advancer(); };
Последний шаг состоит в том, чтобы преобразовать асинхронные функции в функции, которые принимают один параметр (обратный вызов). Это позволяет выполнять итерацию экземпляра генератора, зная, что выражение yield всегда ожидает один параметр — обратный вызов, который используется для дальнейшего продвижения итерации.
/** * Преобразует функцию, которая принимает несколько аргументов в * функция, которая принимает только последний аргумент * исходной функции. * * @param {Function} * @param {...*} */ const curry = (method, ...args) => { return (callback) => { args.push(callback); return method.apply({}, args); }; };
Конечным результатом является сценарий без слишком большого количества уровней вложенных обратных вызовов и с независимыми строками (выполнение одной операции больше не привязано к выполнению последующим после нее).
const foo = (name, callback) => { setTimeout(() => { callback(name); }, 100); }; const curry = (method, ...args) => { return (callback) => { args.push(callback); return method.apply({}, args); }; }; const controller = (generator) => { const iterator = generator(); const advancer = (response) => { var state; state = iterator.next(response); if (!state.done) { state.value(advancer); } } advancer(); }; controller(function* () { const a = yield curry(foo, 'a'); const b = yield curry(foo, 'b'); const c = yield curry(foo, 'c'); console.log(a, b, c); }); // a // b // c
Другой пример: Обработка ошибок
Обычно ошибки обрабатываются для каждой отдельной асинхронной операции, например:
const foo = (name, callback) => { callback(null, name); }; foo('a', (error1, result1) => { if (error1) { throw new Error(error1); } foo('b', (error2, result2) => { if (error2) { throw new Error(error2); } foo('c', (error3, result3) => { if (error3) { throw new Error(error3); } console.log(result1, result2, result3); }); }); }); // a // b // c
В следующем примере я разрешаю контроллеру выдавать ошибку и использую блок try … catch для захвата всех ошибок.
const foo = (parameters, callback) => { setTimeout(() => { callback(parameters); }, 100); }; const curry = (method, ...args) => { return (callback) => { args.push(callback); return method.apply({}, args); }; }; const controller = (generator) => { const iterator = generator(); const advancer = (response) => { if (response && response.error) { return iterator.throw(response.error); } const state = iterator.next(response); if (!state.done) { state.value(advancer); } } advancer(); }; controller(function* () { let a, b, c; try { a = yield curry(foo, 'a'); b = yield curry(foo, {error: 'Something went wrong.'}); c = yield curry(foo, 'c'); } catch (e) { console.log(e); } console.log(a, b, c); }); // Something went wrong. // a undefined undefined
Обратите внимание, что выполнение было прервано до вызова curry(foo, ‘c’).
Еще пример: Бесконечный ряд
Если вы хотите создать бесконечный ряд, вы можете создать генератор для этого. Он будет включать цикл while, и когда вы сделаете это, вы сможете использовать любых другие функции, которые вам понадобятся, для извлечения необходимых значений. Например давайте сгенерируем последовательность Фибоначчи.
function* fibonacci() { let previous = 0 let i = 1; while (true) { [i, previous] = [previous, i + previous] yield previous; } }
И, чтобы взять первые десять элементов последовательности, мы можем написать генератор для этого.
function * take(iterable, n) { let i = 0; for (let value of iterable) { yield value; i++; if (i >= n) { break; } } }
После этого мы можем получить первые десять значений последовательности Фибоначчи.
const iterator = take(fibonacci(), 10); console.log([...iterator]); // -> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Заключение
Итераторы и генераторы являются базовым функционал в JavaScript. Это выразительный способ как генерации значений так и способ перебора значение. Их необходимо хорошо знать, так как они активно используются многими программистами для решения множества задач. Надеюсь эта статья заинтересовало вас узнать о об этом больше. Бесплатная книга Exploring ES6 имеет главу о Generators. Axel Rauschmayer написал о генераторах гораздо больше, чем мне удалось осветить в этой статье. Рекомендую эту книга к прочтению.
Источники используемые в написание поста:
Sal Rahman — Elegant iteration in JavaScript with generators (https://dev.to/manlycoffee/elegant-javascript-with-generators-1720)
Gajus Kuizinas — The definitive guide to the JavaScript generators (https://dev.to/gajus/the-definitive-guide-to-the-javascript-generators-1deo)
Практические вопросы на закрепление материала по теме Генераторы
P.S. Так же на гитхабе есть неплохой образовательный проект JavaScript ES(6|2015) generators workshopper. Learn in practice. 🤘