События против потоков против наблюдаемых против асинхронных итераторов

В настоящее время единственным стабильным способом обработки серии асинхронных результатов в JavaScript является использование системы событий. Тем не менее, разрабатываются три альтернативы:

Потоки: https://streams.spec.whatwg.org
Наблюдаемые: https://tc39.github.io/proposal-observable
Асинхронные итераторы: https://tc39.github.io/proposal-async-iteration

Каковы различия и преимущества каждого по сравнению с событиями и другими?

Кто-нибудь из них намеревается заменить события?


person Daniel Herr    schedule 11.09.2016    source источник
comment
Кстати, внимательно изучите эту статью: Общая теория реактивности   -  person    schedule 12.09.2016
comment
Трудно представить себе лучший пример увлекательного, полезного вопроса, который, тем не менее, согласно нелепым, жестко сфинктерным правилам SO должен быть закрыт как слишком широкий или вопрос мнения.   -  person    schedule 03.04.2018


Ответы (2)


Здесь есть примерно две категории API: pull и push.

Вытащить

API-интерфейсы асинхронного извлечения хорошо подходят для случаев, когда данные извлекаются из источника. Этим источником может быть файл, или сетевой сокет, или список каталогов, или что-то еще. Суть в том, что работа выполняется для извлечения или создания данных из источника по запросу.

Асинхронные итераторы являются здесь базовым примитивом, предназначенным для общего воплощения концепции асинхронного источника на основе запроса. В таком источнике вы:

  • Вытяните из асинхронного итератора, выполнив const promise = ai.next()
  • Дождитесь результата, используя const result = await promise (или используя .then())
  • Проверьте результат, чтобы узнать, является ли это исключением (выброшено), промежуточным значением ({ value, done: false }) или сигналом готовности ({ value: undefined, done: true }).

Это похоже на то, как итераторы синхронизации являются универсальным проявлением концепции источника значений синхронизации на основе запроса. Шаги для итератора синхронизации точно такие же, как и выше, за исключением шага «ожидание результата».

Читаемые потоки — это особый случай асинхронных итераторов, предназначенный специально для инкапсуляции источников ввода-вывода, таких как сокеты/файлы/и т.д. У них есть специализированные API для передачи их в доступные для записи потоки (представляющие другую половину экосистемы ввода-вывода, приемники) и обработки возникающего обратного давления. Они также могут быть специализированы для обработки байтов эффективным способом «принеси свой собственный буфер». Все это чем-то напоминает массивы — частный случай итераторов синхронизации, оптимизированных для индексированного доступа O(1).

Еще одна особенность API-интерфейсов pull заключается в том, что они, как правило, предназначены для одного потребителя. Тот, кто извлекает значение, теперь имеет его, и его нет в исходном асинхронном итераторе/потоке/и т. д. больше. Это было оторвано потребителем.

В общем, API-интерфейсы pull предоставляют интерфейс для связи с некоторым базовым источником данных, позволяя потребителю проявить к нему интерес. Это в отличие от...

Толкать

Push-API хорошо подходят, когда что-то генерирует данные, а генерируемые данные не заботятся о том, хотят они кого-то или нет. Например, неважно, интересно ли это кому-то, все равно верно, что ваша мышь двигалась, а потом вы куда-то щелкнули. Вы хотели бы манифестировать эти факты с помощью push-API. Затем потребители — возможно, несколько из них — могут подписаться на push-уведомления о таких вещах.

Сам API не заботится о том, подписывается ли ноль, один или много потребителей. Это просто проявление факта о вещах, которые произошли во Вселенной.

События — простое проявление этого. Вы можете подписаться на EventTarget в браузере или EventEmitter в Node.js и получать уведомления об отправленных событиях. (Обычно, но не всегда, создателем EventTarget.)

Observables — это более совершенная версия EventTarget. Их основное нововведение заключается в том, что сама подписка представлена ​​первоклассным объектом, Observable, к которому затем можно применять комбинаторы (такие как фильтр, карта и т. д.). Они также принимают решение объединить три сигнала (условно называемых следующим, завершенным и ошибочным) в один и придать этим сигналам особую семантику, чтобы комбинаторы уважали их. Это отличается от EventTarget, где имена событий не имеют специальной семантики (ни один метод EventTarget не заботится о том, называется ли ваше событие «complete» или «asdf»). EventEmitter в Node имеет некоторую версию этого подхода с особой семантикой, когда события «ошибки» могут привести к сбою процесса, но это довольно примитивно.

Еще одна приятная особенность наблюдаемых над событиями заключается в том, что, как правило, только создатель наблюдаемых может заставить их генерировать эти сигналы next/error/complete. В то время как в EventTarget любой может вызвать dispatchEvent(). По моему опыту, такое разделение обязанностей делает код лучше.

Но, в конце концов, и события, и наблюдаемые объекты являются хорошими API для передачи вхождений в мир подписчикам, которые могут настроиться и отключиться в любое время. Я бы сказал, что observables — более современный способ сделать это и в некоторых отношениях лучше, но события более широко распространены и хорошо понятны. Так что если что-то и предназначалось для замены событий, так это наблюдаемые объекты.

Нажать ‹-> потянуть

Стоит отметить, что в крайнем случае вы можете построить любой подход поверх другого:

  • Чтобы построить push поверх pull, постоянно извлекайте из pull API, а затем рассылайте куски любым потребителям.
  • Чтобы построить pull поверх push-а, немедленно подпишитесь на push-API, создайте буфер, который накапливает все результаты, и когда кто-то вытягивает, извлекайте их из этого буфера. (Или подождите, пока буфер не станет непустым, если ваш потребитель извлекает быстрее, чем обернутый push-API.)

Последнее, как правило, требует написания гораздо большего количества кода, чем первое.

Еще один аспект попытки адаптироваться между ними заключается в том, что только API-интерфейсы pull могут легко передавать обратное давление. Вы можете добавить побочный канал для отправки API-интерфейсов, чтобы они могли передавать противодавление обратно в источник; Я думаю, что Dart делает это, и некоторые люди пытаются создавать эволюции наблюдаемых объектов, обладающих этой способностью. Но это IMO гораздо более неудобно, чем просто правильный выбор API-интерфейса. Обратной стороной этого является то, что если вы используете push API для предоставления источника, основанного на принципе pull, вы не сможете сообщить о противодавлении. Кстати, это ошибка, допущенная с API-интерфейсами WebSocket и XMLHttpRequest.

В общем, я считаю попытки унифицировать все в один API, обернув другие, ошибочными. Push и pull имеют разные, не очень пересекающиеся области, в которых каждая из них работает хорошо, и говорить, что мы должны выбрать один из четырех API, которые вы упомянули, и придерживаться его, как это делают некоторые люди, недальновидно и приводит к неудобному коду.

person Domenic    schedule 10.11.2017
comment
Не могли бы вы уточнить, что вы подразумеваете под обратным давлением? - person Daniel Rosenwasser; 10.11.2017
comment
Дэниел: См., например, www.reactivestreams.org. - person Viktor Klang; 10.11.2017
comment
@Domenic Это ошибка, допущенная с API XMLHttpRequest, кстати, не могли бы вы описать ее более подробно, спасибо! - person SmallTown NE; 11.11.2017
comment
Как XMLHttpRequest является push-API? - person Frederik Krautwald; 17.11.2017
comment
Потому что он использует события, чтобы передать вам данные, вместо того, чтобы ждать, пока вы прочитаете часть данных. Таким образом, у него нет понятия обратного давления, поскольку он понятия не имеет, как быстро вы потребляете данные. - person Domenic; 18.11.2017
comment
Отличный ответ Доменик - вы можете добавить несколько примеров из gtor или аналогичного ресурса для примеров pull/push. Для будущих читателей стоит упомянуть, что Node на данный момент намерен взаимодействовать с асинхронными итераторами (но не с наблюдаемыми), поскольку они находятся намного дальше в спецификации. - person Benjamin Gruenbaum; 20.11.2017
comment
@Domenic Я знаю, что этот пост довольно старый. Но я сомневаюсь, когда читаю, что только API-интерфейсы pull могут легко передавать обратное давление. Решают ли реактивные потоки эту проблему, обеспечивая неблокирующее противодавление и по-прежнему соблюдают идиоматический push-API? Не так ли? - person Miguel Gamboa; 07.08.2019
comment
На мой взгляд, нет. Как я сказал в посте: вы можете добавить побочный канал для push-API, чтобы они могли передавать противодавление обратно в источник; Я думаю, что Dart делает это, и некоторые люди пытаются создавать эволюции наблюдаемых объектов, обладающих этой способностью. Но это IMO гораздо более неудобно, чем просто правильный выбор API-интерфейса. - person Domenic; 08.08.2019
comment
Потоки записи используют отдельные события для передачи обратного давления в NodeJS, что является примером использования стороннего канала. Наблюдаемые объекты не имеют никакого механизма обратного давления, у них есть только операторы, которые позволяют вам не быть заваленными фильтрацией на основе скоростей или ожиданием того, что другой наблюдаемый объект выдаст сигнал о готовности, но это еще один пример побочного канала. - person Arlen Beiler; 11.09.2019
comment
Это становится настолько запутанным, что API Obervables очень похож на iterator-helpers и предложение-эмитент, не говоря уже о потоках узла/пользователя. И по сути они делают одно и то же. - person dy_; 30.10.2019

Мое понимание асинхронных итераторов немного ограничено, но насколько я понимаю, потоки WHATWG — это особый случай асинхронных итераторов. Для получения дополнительной информации об этом см. Часто задаваемые вопросы по Streams API. В нем кратко рассматривается, как отличается от Observables.

И асинхронные итераторы, и наблюдаемые объекты являются общими способами управления несколькими асинхронными значениями. На данный момент они не взаимодействуют, но, похоже, создание Observables из асинхронных итераторов считается. Наблюдаемые по своей природе, основанной на отправке, гораздо больше похожи на текущую систему событий, а AsyncIterables основаны на вытягивании. Упрощенный вид будет:

-------------------------------------------------------------------------    
|                       | Singular         | Plural                     |
-------------------------------------------------------------------------    
| Spatial  (pull based) | Value            | Iterable<Value>            |    
-------------------------------------------------------------------------    
| Temporal (push based) | Promise<Value>   | Observable<Value>          |
-------------------------------------------------------------------------    
| Temporal (pull based) | await on Promise | await on Iterable<Promise> |
-------------------------------------------------------------------------    

Я представил AsyncIterables как Iterable<Promise>, чтобы упростить аналогию. Обратите внимание, что await Iterable<Promise> не имеет смысла, так как его следует использовать в цикле for await...of AsyncIterator.

Вы можете найти более полное объяснение Kriskowal: общая теория реактивности.

person kirly    schedule 02.11.2017
comment
Я чувствую, что ваш ответ полезен для сравнения на высоком уровне, но я не согласен с утверждением, что AsyncIterables являются Iterable<Promise>. Iterable<Promise> — это синхронная итерация промисов, не имеющая понятия обратного давления. Вы можете потреблять его так быстро, как хотите, без проблем. AsyncIterables имеют обратное давление, что означает, что вызов next() на итераторе до того, как предыдущая итерация установится, является незаконным. Он дает Promise<{ value, done }>, а не { Promise<value>, done }, как это делает синхронный итератор промисов. - person Patrick Roberts; 27.03.2019
comment
О, интересное отличие. Я не думал об этом раньше. Интересно, как должен обрабатываться следующий вызов. Вернуть то же обещание? Сбросить ошибку? - person Arlen Beiler; 11.09.2019
comment
Поскольку Observables основаны на push, им легко постоянно извлекать из AsyncIterator и испускать так быстро, как только может. - person Arlen Beiler; 11.09.2019