Начиная с ES 2018 (последние браузеры или Node 10) JavaScript поддерживает асинхронные генераторы. Генераторы — это функции, которые возвращают набор значений, по одному значению за раз. Эти значения могут обрабатываться внутри кода, вызывающего генератор немедленно, как только они становятся доступными. Нет необходимости ждать, пока сначала будет составлен весь набор результатов. В случаях, когда набор результатов огромен или вообще никогда не заканчивается, это довольно удобно. Результат одной функции-генератора можно передать другой функции, которая также может быть функцией-генератором. И так далее. Это делает возможной конвейерную обработку: ряд функций, работающих вместе (и более или менее параллельно) для прохождения каждого результата через ряд шагов обработки.
С относительно недавним добавлением асинхронных генераторов функция генератора, создающая набор результатов, может быть асинхронной — полагаясь на пример Promises для сбора своих значений.
В этой статье я хочу показать кое-что из красоты всего этого. Я поделюсь простым приложением ES 2018/Node, которое использует промисы для асинхронного создания значений, запускаемых тайм-аутами. Three Promises представляют собой три датчика температуры; в этом случае значения просто генерируются. Однако эти промисы могут также считывать значения из внешнего источника или потреблять входящие события. Каждое обещание, когда оно разрешено, производит показания датчика. Промис заключен в промис, который записывает значение датчика во временное хранилище ( latestValue) и удаляет себя из пула датчиков — набор обещаний, которые функция sensorValues() ожидает при использовании Promise.race([ …сенсорный пул])
В функции асинхронного генератора sensorValues() мы в бесконечном цикле ждем разрешения одного или sensorPromises (Promise.race разрешается до первого из набора обещаний для разрешения). Когда это происходит, выдается latestValue, записанное при разрешении обещания сенсора.
Другая функция асинхронного генератора — runningSensorAverages — запускается выходом из sensorValues (в цикле for await (sensorReading of sensorReadings)). Полученное значение добавляется к коллекции значений для текущего датчика на карте датчиков. Значение тиков увеличено; ticks подсчитывает количество значений, полученных с момента последнего вычисления работающего агрегата. Если значение тиков равно значению периода (параметр, указывающий, через сколько значений должен быть рассчитан новый агрегат), то новый агрегат вычисляется с использованием последние значения windowSize в коллекции значений для текущего датчика. Вычисленное значение выдается (и тики сбрасываются).
Полученный текущий агрегат принимается в функции doIt(). Эта функция записывает полученное значение в консоль — из другого цикла for await.
Результат выглядит следующим образом:
Конвейерную природу этого приложения лучше всего отражает эта строка:
for await (runningAverage of runningSensorAverages(filterOutliersFromSensorReadings( sensorValues()), 15, 10)) {..}
Результат потоковой передачи от sensorValues() передается — по одному чтению за раз — в функцию фильтра, а вывод этой функции — в runningSensorAverages, чьи выходные данные отображаются как последующие значения в цикле for await.
Добавление агрегатов с временным окном
Пока мы этим занимаемся, давайте добавим агрегаты с временным окном: средние значения создаются каждые X секунд.
Реализация выполняется с использованием кеша — временного хранилища показаний датчиков, которое записывается функцией runningSensorAverages(). Функция timeWindowedAggregates() запускается по тайм-ауту после периода, указанного параметром timeWindow. Когда функция «просыпается», она считывает текущее содержимое из кеша, вычисляет и выдает средние значения.
Функция doIt2() содержит цикл по генератору timeWindowedAggregates(): await for (timedWindowAggregate of timeWindowedAggregates(6000)), который выводит средние значения на консоль.
Комбинированный вывод выглядит следующим образом:
Обратите внимание, что все усреднения временных окон производятся в одно и то же время (по различному количеству показаний между датчиками), а текущие агрегаты производятся в разное время (по одному и тому же количеству показаний).
Расширенная кодовая база:
Ресурсы
Итерация частичных результатов Promise.all — https://agentcooper.io/iterate-promise-all/
Массивы JavaScript — поиск минимума, максимума, суммы и среднего значения — https://codeburst.io/javascript-arrays-finding-the-minimum-maximum-sum-average-values-f02f1b0ce332
Скользящее среднее (Википедия) — https://en.wikipedia.org/wiki/Moving_average
Как заставить ваши функции JavaScript спать — https://flaviocopes.com/javascript-sleep/
Javascript — Generator-Yield/Next и Async-Await — https://codeburst.io/javascript-generator-yield-next-async-await-e428b0cb52e4
Асинхронные генераторы и конвейеры в JavaScript — https://dev.to/nestedsoftware/asynchronous-generators-and-pipelines-in-javascript-1h62
Давайте поэкспериментируем с функциональными генераторами и оператором конвейера в JavaScript — https://medium.freecodecamp.org/lets-experiment-with-functional-generators-and-the-pipeline-operator-in-javascript-520364f97448
Первоначально опубликовано на https://technology.amis.nl 30 апреля 2019 г.