Здесь есть примерно две категории 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