Я вижу много вопросов по темам на Stack Overflow. Недавно я видел один, в котором спрашивали, как следует использовать AsyncSubject. Этот вопрос побудил меня написать эту статью, чтобы показать, почему необходимы различные типы предметов и как они используются в самом RxJS.

Каковы варианты использования предметов?

В своей статье О предметах Бен Леш утверждает, что:

… [Многоадресная передача] - это основной вариант использования субъектов в RxJS.

Мы рассмотрим многоадресную рассылку более подробно позже в этой статье, но пока достаточно знать, что она включает в себя получение уведомлений от одного наблюдаемого источника и их пересылку одному или нескольким наблюдателям назначения.

Это соединение наблюдателей с наблюдаемым - вот что суть предмета. Они могут это делать, потому что сами субъекты одновременно являются наблюдателями и наблюдаемыми.

Как можно использовать предметы?

Давайте возьмем в качестве примера компонент Angular: awesome-component. Наш компонент делает несколько замечательных вещей и имеет внутренний наблюдаемый объект, который генерирует значения, когда пользователь взаимодействует с компонентом.

Чтобы родительские компоненты могли подключаться к наблюдаемому, awesome-component принимает свойство ввода observer, которое подписывается на наблюдаемое. Это означает, что родитель может подключиться к наблюдаемому, указав наблюдателя, например:

Когда наблюдатель подключен, родитель подключен и получает значения от awesome-component. Однако это по сути то же самое, как если бы awesome-component испустил свои значения с помощью выходного события. Так почему бы не использовать событие?

У наблюдаемых есть то преимущество, что ими легко манипулировать. Например, легко добавить фильтрацию и устранение ошибок, просто применив несколько операторов. Но у родительского компонента есть наблюдатель, а не наблюдаемое, так как мы можем применять операторы?

Субъекты являются как наблюдателями, так и наблюдаемыми, поэтому, если мы создадим Subject, его можно будет передать awesome-component (как наблюдатель), и к нему может быть применено противодействие (как наблюдаемое), например:

Субъект связывает наблюдателя, выполняющего что-то со значением, с наблюдаемым awesome-component, но с применением операторов, выбранных родительским компонентом.

Составление различных наблюдаемых

Используя Subject для составления наблюдаемого, awesome-component может использоваться разными компонентами по-разному. Например, другой компонент может интересоваться только последним переданным значением. Этот компонент может использовать оператор last:

Интересно, что есть еще один способ, которым компонент может выбрать получение только последнего отправленного значения из awesome-component: он может использовать другой тип объекта. AsyncSubject испускает только последнее полученное значение, поэтому альтернативной реализацией будет:

Если использование AsyncSubject эквивалентно составлению наблюдаемого с использованием оператора Subject и last, зачем усложнять RxJS классом AsyncSubject?

Дело в том, что темы предназначены в первую очередь для многоадресной рассылки.

Здесь они эквивалентны, потому что есть один подписчик - наблюдатель, который делает что-то со значением. В ситуации многоадресной рассылки может быть несколько подписчиков, и применение оператора last к Subject не приведет к тому же поведению, что и AsyncSubject, для поздних подписчиков.

Давайте подробнее рассмотрим многоадресную рассылку.

Как темы используются в RxJS?

Ядро инфраструктуры многоадресной рассылки RxJS реализуется с помощью одного оператора: multicast. Оператор multicast применяется к наблюдаемому источнику, берет предмет (или фабрику, создающую предмет) и возвращает наблюдаемое, составленное из предмета.

Оператор multicast чем-то похож на awesome-component в наших примерах: мы можем получить наблюдаемое, которое демонстрирует другое поведение, просто передав другой тип объекта.

Когда базовый Subject передается в multicast:

  • подписчики многоадресного наблюдаемого получают уведомления next, error и complete источника; а также
  • поздние подписчики, то есть те, которые подписываются после того, как было отправлено уведомление error или complete, получают уведомление error или complete.

Важно отметить, что до тех пор, пока multicast не будет передан фабрике, поздние подписчики не произведут новую подписку на источник.

Чтобы составить многоадресный наблюдаемый объект, который пересылает последнее отправленное next уведомление исходного объекта наблюдения всем подписчикам, недостаточно применить оператор last к многоадресному наблюдаемому объекту, который был создан с использованием Subject. Поздние подписчики на такое наблюдаемое не получат последнее отправленное next уведомление; они получат только complete уведомление.

Чтобы опоздавшие подписчики получили последнее отправленное next уведомление, оно должно быть сохранено в состоянии субъекта. Это то, что делает AsyncSubject, и поэтому класс AsyncSubject необходим.

А как насчет других предметных классов?

Есть еще два варианта темы: BehaviorSubject и ReplaySubject.

Чтобы понять BehaviorSubject, давайте взглянем на другой компонентный пример:

Здесь родительский компонент подключается к awesome-component с помощью Subject и применяет оператор startWith. Использование startWith гарантирует, что родитель получит значение "awesome" при подписке, за которым следуют значения, выдаваемые awesome-component - всякий раз, когда они будут выданы.

Точно так же, как AsyncSubject заменил использование оператора Subject и last, BehaviorSubject может заменить использование оператора Subject и startWith - конструктор BehaviorSubject принимает значение, которое в противном случае было бы передано startWith.

Однако использование операторов Subject и startWith не повлияет на желаемое поведение в ситуации с несколькими абонентами. Первый подписчик увидит ожидаемое поведение, но последующие подписчики всегда будут получать значение startWith, даже если источник уже отправил значение.

Если используется BehaviorSubject, последующие подписчики получат начальное значение, если источник еще не отправил, или последнее отправленное значение, если оно было. Это возможно, потому что BehaviorSubject сохраняет значение в своем состоянии.

Для ReplaySubject нет аналогии с одним подписчиком, поскольку концепция воспроизведения уже полученных уведомлений по своей сути является многопользовательской. Чтобы облегчить воспроизведение уведомлений для последующих подписчиков, ReplaySubject сохраняет уведомления в своем состоянии.

Так как же использовать эти предметы?

Теперь, когда мы увидели, что делают различные предметы и почему они необходимы, как их следует использовать? Что ж, вполне вероятно, что единственными предметными классами, которые вам когда-либо понадобятся, будут Subject и BehaviorSubject.

Subject отлично подходит для подключения наблюдателя к наблюдаемому, а BehaviorSubject хорошо подходит для представления атома состояния. А для ситуаций с многоадресной рассылкой есть альтернативы использованию темы.

RxJS содержит операторы многоадресной рассылки, которые используют различные предметные классы, и точно так же, как я предпочитаю использование наблюдаемых создателей RxJS (например, fromEvent), а не вызовов Observable.create, для ситуаций многоадресной рассылки я предпочитаю использовать операторы RxJS, а не явные темы:

  • publish или share можно использовать вместо Subject;
  • publishBehavior можно использовать вместо BehaviorSubject;
  • publishLast можно использовать вместо AsyncSubject; а также
  • publishReplay или shareReplay можно использовать вместо ReplaySubject.

Более подробно операторы publish и share описаны в моих статьях:

Этот пост также опубликован в моем личном блоге: ncjamieson.com.