Что такое Генераторы в JavaScript

Spread the love

На прошлой недели в этом блоге, я опубликовал статью о Итераторах (Как перестать бояться и полюбить Итераторы). Итераторы часто используются вместе генераторами. Поэтому очевидным продолжением того поста, должна быть статья о Генераторах в 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)

Практические вопросы на закрепление материала по теме Генераторы

1. Что будет в результате:

function* generator(i) {
  yield i;
  yield i * 2;
}

const gen = generator(10);

console.log(gen.next().value);
console.log(gen.next().value);
 
 
 
 

2. Что будет в результате выполнения:

function* generatorOne() {
  yield ['a', 'b', 'c'];
}

function* generatorTwo() {
  yield* ['a', 'b', 'c'];
}

const one = generatorOne()
const two = generatorTwo()

console.log(one.next().value)
console.log(two.next().value)
 
 
 
 

Вопрос 1 из 2

P.S. Так же на гитхабе есть неплохой образовательный проект JavaScript ES(6|2015) generators workshopper. Learn in practice. 🤘

Была ли вам полезна эта статья?
[4 / 5]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments