Далее читателю предложена статья, которая, в случае положительного отклика, может перерасти в цикл. В случае успешного написания мной этого цикла, а Читателем его успешного освоения, про следующий код будет понятно не только то, что он делает, но и как устроен под капотом:
while (true) { const data = yield getNextChunk(); // вызов асинхронной логики const processed = processData(data); try { yield sendProcessedData(processed); showOkResult(); } catch (err) { showError(); } }
Это первая, пилотная часть: Итераторы и Генераторы.
Итераторы
Итак, итератор — это интерфейс, предоставляющий последовательный доступ к данным.
Как видно, в определении ничего не сказано о структурах данных или памяти. Действительно, последовательность undefined-ов может быть представлена в виде итератора, при этом не занимать места в памяти.
Предлагаю читателю ответить на вопрос: является ли массив итератором?
Зачем же тогда нужны итераторы, если массив, одна из базовых структур языка, позволяет работать с данными и последовательно, и в произвольном порядке?
Представим, что нам нужен итератор, который реализует последовательность натуральных чисел. Или чисел Фибоначчи. Или любую другую бесконечную последовательность. В массиве сложно разместить бесконечную последовательность, понадобится механизм постепенного наполнения массива данными, а также изъятия старых данных, чтобы не заполнить всю память процесса. Это излишнее усложнение, которое несёт с собой дополнительную сложность реализации и поддержки, при том, что решение без массива может уместиться в несколько строчек:
const getNaturalRow = () => { let current = 0; return () => ++current; };
Также итератором можно представить получение данных из внешнего канала, например websocket.
В javascript итератором является любой объект, у которого есть метод next(), который возвращает структуру с полями value — текущее значение итератора и done — флагом, указывающим на завершение последовательности (эта договорённость описана в стандарте языка ECMAScript). Такой объект реализует интерфейс Iterator. Перепишем прошлый пример в этом формате:
const getNaturalRow = () => ({ _current: 0, next() { return { value: ++this._current, done: false, }}, });
В javascript также есть интерфейс Iterable — это объект, который имеет метод @@iterator (данная константа доступна как Symbol.iterator), который возвращает итератор. Для объектов, реализующих такой интерфейс, доступен обход с помощью оператора for..of. Перепишем наш пример ещё раз, только в этот раз как реализацию Iterable:
const naturalRowIterator = { [Symbol.iterator]: () => ({ _current: 0, next() { return { value: ++this._current, done: this._current > 3, }}, }), } for (num of naturalRowIterator) { console.log(num); } // Вывод: 1, 2, 3
Как можно видеть, нам пришлось сделать так, чтобы флаг done в какой-то момент стал положительным, иначе бы цикл был бесконечным.
Генераторы
Следующим этапом эволюции итераторов стали генераторы. Они предоставляют синтаксический сахар, позволяющий возвращать значения итератора будто значение функции. Генератор — это функция (объявляется со звёздочкой: function*), возвращающая итератор. При этом итератор не возвращается явно, в функции лишь возвращаются значения итератора с помощью оператора yield. Когда функция заканчивает своё выполнение, итератор считается завершённым (результаты последующих вызовов метода next будут иметь флаг done равным true)
function* naturalRowGenerator() { let current = 1; while (current <= 3) { yield current; current++; } } for (num of naturalRowGenerator()) { console.log(num); } // Вывод: 1, 2, 3
Уже в этом простом примере невооружённым глазом виден главный нюанс генераторов: код внутри функции генератора не выполняется синхронно. Выполнение кода генератора происходит поэтапно, в результате вызовов next() у соответствующего итератора. Рассмотрим, как выполняется код генератора на прошлом примере. Специальным курсором будем отмечать, где остановилось выполнение генератора.
В момент вызова naturalRowGenerator создаётся итератор.
function* naturalRowGenerator() { ▷let current = 1; while (current <= 3) { yield current; current++; } }
Далее, когда мы первые три раза вызываем метод next или, в нашем случае, проходим итерации цикла, курсор встаёт после оператора yield.
function* naturalRowGenerator() { let current = 1; while (current <= 3) { yield current; ▷ current++; } }
И на все последующие вызовы next и после выхода из цикла генератор завершает своё выполнение и, результатами вызова next будет { value: undefined, done: true }
Передача параметров в итератор
Представим, что в наш итератор натуральных чисел нужно добавить возможность сбрасывать текущий счётчик и начинать отчёт с начала.
naturalRowIterator.next() // 1 naturalRowIterator.next() // 2 naturalRowIterator.next(true) // 1 naturalRowIterator.next() // 2
Понятно как обработать такой параметр в самописном итераторе, но как быть с генераторами?
Оказывается, генераторы поддерживают передачу параметров!
function* naturalRowGenerator() { let current = 1; while (true) { const reset = yield current; if (reset) { current = 1; } else { current++; } } }
Переданный параметр становится доступен как результат оператора yield. Попробуем добавить ясности с помощью подхода с курсором. В момент создания итератора ничего не поменялось. Далее следует первый вызов метода next():
function* naturalRowGenerator() { let current = 1; while (true) { const reset = ▷yield current; if (reset) { current = 1; } else { current++; } } }
Курсор замер на моменте возврата из оператора yield. При следующем вызове next, переданное в функцию значение установит значение переменной reset. Куда же попадёт значение, переданное в самый первый вызов next, ведь там же ещё не было вызова yield? Никуда! Растворится в просторах garbage collector-а. Если нужно передать какое-то начальное значение в генератор, то это можно сделать с помощью аргументов самого генератора. Пример:
function* naturalRowGenerator(start = 1) { let current = start; while (true) { const reset = yield current; if (reset) { current = start; } else { current++; } } } const iterator = naturalRowGenerator(10); iterator.next() // 10 iterator.next() // 11 iterator.next(true) // 10
Заключение
Мы рассмотрели концепцию итераторов и её реализацию в языке javascript. Также мы изучили генераторы — синтаксическую конструкцию для удобной реализации итераторов.
Хотя в данной статье я приводил примеры с числовыми последовательностями, итераторы в javascript позволяют решить намного больше задач. С помощью них можно представить любую последовательность данных и даже многие конечные автоматы. В следующей статье я хотел бы рассказать о том, как можно использовать генераторы для построения асинхронных процессов (coroutines, goroutines, csp и т. д.).
ссылка на оригинал статьи https://habr.com/ru/post/522864/
Добавить комментарий