JavaScript

Subject — это «особый» тип наблюдаемого объекта, который позволяет транслировать значения сразу нескольким подписчикам. Самое классное здесь то, что Subject предоставляет ответы в режиме реального времени.

Например, у нас есть субъект (Subject) с 10 подписчиками. Тогда при отправлении значений субъекту мы видим, как они захватываются каждым подписчиком.

Это приводит к возникновению ряда трудностей. Что, если сначала мы передадим значение, а потом подпишемся, или наоборот? Время всегда играет важную роль. Если подписаться поздно, то мы не сможем получить доступ к значениям. Это как включать трансляцию матча по телевизору на 30 минут позже.

К счастью, есть 4 типа Subject, позволяющих «путешествовать во времени» и получать доступ к значениям, вне зависимости от времени подписки.

Вот, о чем мы поговорим:

  1. Что такое Subject: практические примеры его использования.
  2. BehaviorSubject: получить последнее сообщение.
  3. ReplaySubject: путешествие во времени.
  4. AsyncSubject: получить последнее сообщение по завершению.

1. Что такое Subject?

Как уже говорилось выше, Subject — это не более, чем наблюдаемый объект с рядом характеристик. Сам по себе наблюдаемый объект является вызываемой коллекцией, которая генерирует данные при подписке. А Subject — это как раз то место, где мы управляем состоянием «в какой момент времени сгенерировать данные» для нескольких подписчиков.

Subject может вызывать методы .next().complete()и.error() снаружи. Тогда как в наблюдаемом объекте данные методы доступны в форме обратных вызовов.

// Создание наблюдаемого объекта
const observable = new Observable((observer) => {
    observer.next(10);
    observer.next(5);
    observer.complete();
});

// Создание Subject
const subject = new Subject();
subject.next(10);
subject.next(5);
subject.complete();

Subject vs наблюдаемый объект

Практический пример. Давайте создадим простую чат-группу с помощью Subject

Допустим, мы создаем простое приложение-чат, в котором люди могут отправлять сообщения в группу. Наш первый шаг — создание экземпляра Subject и его присваиваниеchatGroup.

// Создание субъекта "Observable"
const chatGroup = new Subject();

Создание Subject (чат-группа)

Теперь, когда мы создали чат-группу (Subject), можно переходить к добавлению сообщений. Создадим типичную беседу между двумя друзьями.

// Передача значений в поток
chatGroup.next('David - Hi, which hot series do you recommend?');
chatGroup.next('Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones');
chatGroup.next('David - Interesting, which one is the hottest?');
chatGroup.next('Peter - Game of Thrones!');

Добавление сообщений в чат-группу

Пока все отлично: в группе уже есть 4 сообщения. Так что же случится, если мы подпишемся? Или, допустим, новый друг, Джон, захотел присоединиться к разговору. Увидит ли он старые сообщения?

// Вывод сообщений
chatGroup.subscribe((messages) => {
    console.log(messages)
})

Просмотр беседы (подписка)

К сожалению, нет. Джон не увидит старых сообщений, поскольку добавился к группе слишком поздно. Это отличный пример того, как работает реактивное программирование — осуществляет передачу значений во времени. Поэтому для доступа к данным нужно подписываться вовремя.

Вернемся к нашему примеру. Что, если Джон зашел в группу в середине обсуждения?

// Передача значений в поток
chatGroup.next('David - Hi, which hot series do you recommend?');
chatGroup.next('Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones');

// Джон присоединяется к беседе
chatGroup.subscribe((messages) => {
    console.log(messages)
});

chatGroup.next('David - Interesting, which one is the hottest?');
chatGroup.next('Peter - Game of Thrones!');

// РЕЗУЛЬТАТ
// David - Interesting, which one is the hottest?
// Peter - Game of Thrones!

Джон присоединился к разговору в середине беседы

Как только Джон присоединяется к группе (подписывается), он может видеть два последних сообщения. А Subject делает то, что должен. Но что, если нам нужно, чтобы Джон видел все сообщения… или только последнее… или просто получал уведомление о новых сообщениях?

По сути, все эти типы Subjects во многом похожи. Однако каждый из них дополнен своим функционалом, о котором мы поговорим ниже.

2. BehaviorSubject: получение последнего сообщения

BehaviorSubject идентичен Subject с той лишь разницей, что ему нужно исходное значение в качестве аргумента, который отмечает начальную точку потока данных. Причина в следующем: когда мы подписываемся, BehaviorSubject возвращает последнее сообщение. Концепция та же, что и при работе с массивами: для получения последнего значения мы указываем array.length-1.

import {BehaviorSubject } from "rxjs";

// Создание Subject
const chatGroup = new BehaviorSubject('Starting point');

// Передача значений в поток данных
chatGroup.next('David - Hi, which hot series do you recommend?');
chatGroup.next('Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones');
chatGroup.next('David - Interesting, which one is the hottest?');
chatGroup.next('Peter - Game of Thrones!');

// Джон присоединяется к беседе
chatGroup.subscribe((messages) => {
    console.log(messages)
})

// РЕЗУЛЬТАТ
// Peter - Game of Thrones!

BehaviorSubject

3. ReplaySubject: путешествие во времени

Как следует из названия ReplaySubject, при подписке транслируются все сообщения. Вне зависимости от того, насколько «вовремя» мы подписались на событие. Это как путешествие во времени, когда мы можем получить доступ ко всем переданным значениям.

import { ReplaySubject } from "rxjs";

// Создание Subject
const chatGroup = new ReplaySubject();

// Передача значений в поток данных
chatGroup.next('David - Hi, which hot series do you recommend?');
chatGroup.next('Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones');
chatGroup.next('David - Interesting, which one is the hottest?');
chatGroup.next('Peter - Game of Thrones!');

// Джон присоединяется к беседе
chatGroup.subscribe((messages) => {
    console.log(messages)
})

// РЕЗУЛЬТАТ
// David - Hi, which hot series do you recommend?'
// Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones'
// David - Interesting, which one is the hottest?'
// Peter - Game of Thrones!'

ReplaySubject

4. AsyncSubject: получение последнего сообщения по завершению

AsyncSubject похож на BehaviorSubject. Он также передает последнее значение при подписке. Однако в данном случае ему нужен метод complete(). Этот метод отмечает завершение потока. Как только поток завершен, генерируется последнее значение.

import { AsyncSubject } from "rxjs";

// Создание Subject
const chatGroup = new AsyncSubject();

// Передача значений в поток данных
chatGroup.next('David - Hi, which hot series do you recommend?');
chatGroup.next('Peter - Game of Thrones, Bodyguard or Narcos are few of the good ones');
chatGroup.next('David - Interesting, which one is the hottest?');
chatGroup.next('Peter - Game of Thrones!');

chatGroup.complete(); // <-- Mark the stream as completed

// Джон присоединяется к беседе
chatGroup.subscribe((messages) => {
    console.log(messages)
})

// РЕЗУЛЬТАТ
// Peter - Game of Thrones!'

AsyncSubject

Итог

Теперь в нашем примере с другом Джоном мы сможем сами решать, увидит ли он всю беседу (ReplaySubject), только последнее сообщение (BehaviorSubject), либо же ему придется ограничиться последним сообщением по завершению беседы (AsyncSubject).

Если вкратце, то пределы возможностей реактивного программирования ограничены только вашей фантазией. Конечно же, будут и такие сценарии, в которых главная роль отведется наблюдаемым объектам. Однако понимание Subject и их типов однозначно улучшит ваши навыки в реактивном программировании.

Перевод статьи Dler Ari: An introduction to Subjects in Reactive Programming