Узнайте, как субъекты RxJS используются в реальных приложениях

В этой статье я хочу исследовать тему реализации Subjects в RxJS, утилиты, которая все больше получает признание и любовь сообщества.

В прошлом я использовал Subjects по-разному, но иногда не совсем понимал, что они собой представляют и каковы основные отличия от Observables.

Вот что будет рассказано в статье:

  • Что такое тема?
  • Многоадресная и одноадресная рассылка
  • Другие типы Subject: AsyncSubject, ReplaySubject и BehaviorSubject

Что такое тема?

Начнем с простого вопроса: что такое Subject?
Согласно веб-сайту Rx:

Subject - это особый тип Observable, который позволяет передавать значения множеству Observer'ов.

Сюжеты похожи на EventEmitters.

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

Определение, изложенное в документации по Rx, сначала поразило меня: на самом деле, я всегда думал о Subjects как о способе извлечения и передачи значений с помощью потоков. Оказывается, я не очень хорошо знал их даже после того, как использовал их ежедневно в течение примерно 5 лет.

Совет: с легкостью повторно используйте компоненты Angular / React / Vue в своих проектах с помощью Bit.

Используйте Bit для совместного использования и повторного использования модулей JS и компонентов React / Angular / Vue в разных проектах. Работайте над общими компонентами как одна команда, чтобы вместе быстрее создавать приложения. Пусть Bit сделает всю тяжелую работу, чтобы вы могли легко публиковать, устанавливать и обновлять свои отдельные компоненты без каких-либо накладных расходов. "Нажмите сюда, чтобы узнать больше".

Тема

Subject - это класс, который внутренне расширяет Observable. Subject - это как Observable, так и Observer, что позволяет передавать значения множеству Observers, в отличие от Observables, где каждый подписчик владеет независимым выполнением Observable.

Это означает:

  • вы можете подписаться на Subject, чтобы получать значения из его потока
  • вы можете передавать значения в поток, вызывая метод next()
  • вы даже можете передать Subject в качестве Observer в Observable: как упоминалось выше, Subject также является Observer и как таковой реализует методы next, error и complete

Давайте посмотрим на быстрый пример:

const subject$ = new Subject();
// Pull values
subject$.subscribe(
  console.log, 
  null, 
  () => console.log('Complete!')
);
// Push values
subject$.next('Hello World');
// Use Subject as an Observer
const numbers$ = of(1, 2, 3);
numbers$.subscribe(subject$);
/* Output below */
// Hello Word
// 1
// 2
// 3
// Complete!

Внутренности предмета

Внутри каждый субъект ведет реестр (в виде массива) наблюдателей. Вот как вкратце работает Subject:

  • каждый раз, когда подписывается новый наблюдатель, Subject будет сохранять наблюдателя в массиве наблюдателей.
  • когда генерируется новый элемент (то есть был вызван метод next()), Subject будет перебирать наблюдателей и передавать одно и то же значение каждому из них (многоадресная передача). То же самое произойдет, когда он ошибается или завершает
  • когда Тема завершена, все наблюдатели автоматически отписываются
  • вместо этого, когда Subject отписывается, подписки все еще будут активны. Массив наблюдателей обнуляется, но не отменяет их подписку. Если вы попытаетесь передать значение от неподписанного Subject, на самом деле это вызовет ошибку. Лучшим способом действий должно быть завершение ваших предметов, когда вам нужно избавиться от них и их наблюдателей.
  • когда один из наблюдателей откажется от подписки, он будет удален из реестра.

Многоадресная рассылка

Передача Subject в качестве Observer позволяет преобразовать поведение Observable из одноадресной в многоадресной. Использование Subject - это действительно единственный способ сделать Observable многоадресную рассылку, что означает, что они будут совместно использовать одно и то же выполнение с несколькими Observers.

Но подождите: что на самом деле означает совместное выполнение? Давайте рассмотрим два примера, чтобы лучше понять концепцию.

Давайте возьмем наблюдаемый interval в качестве примера: мы хотим создать наблюдаемый объект, который излучает каждые 1000 мс (1 секунду), и мы хотим поделиться выполнением со всеми подписчиками, независимо от того, когда они подписались.

const subject$ = new Subject<number>();
const observer = {
  next: console.log
};
const observable$ = interval(1000);
// subscribe after 1 second
setTimeout(() => {
  console.log("Subscribing first observer");    
  subject$.subscribe(observer);
}, 1000);
// subscribe after 2 seconds
setTimeout(() => {
  console.log("Subscribing second observer");
  subject$.subscribe(observer);
}, 2000);
// subscribe using subject$ as an observer
observable$.subscribe(subject$);

Подведем итог приведенному выше фрагменту

  • мы создаем субъект с именем subject$ и наблюдателя, который просто записывает текущее значение после каждого выброса
  • мы создаем наблюдаемое, которое излучает каждую 1 секунду (используя interval)
  • подписываемся соответственно через 1 и 2 секунды
  • наконец, мы используем испытуемого в качестве наблюдателя и подписываемся на наблюдаемый интервал

Посмотрим на результат:

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

Другой распространенный пример, показывающий полезность многоадресной рассылки, - это подписка на наблюдаемый объект, который выполняет HTTP-запрос, сценарий, который часто случается в таких фреймворках, как Angular: с помощью многоадресной рассылки наблюдаемого вы можете избежать выполнения нескольких запросов и поделиться выполнением с несколькими подписчиками, который получит такое же значение.

AsyncSubject

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

Короче говоря, AsyncSubject будет:

  • испускать только после завершения
  • испускать только последнее полученное значение
const asyncSubject$ = new AsyncSubject();
asyncSubject$.next(1);
asyncSubject$.next(2);
asyncSubject$.next(3);
asyncSubject$.subscribe(console.log);
// ... nothing happening!
asyncSubject$.complete();
// 3

Как видите, даже если мы подписались, ничего не произошло, пока мы не вызвали метод complete.

ReplaySubject

Прежде чем вводить ReplaySubject, давайте посмотрим на обычную ситуацию, в которой используются обычные субъекты:

  • мы создаем тему
  • где-то в нашем приложении мы начинаем передавать значения субъекту, но подписчика пока нет
  • в какой-то момент первый наблюдатель подписывается
  • мы ожидаем, что наблюдатель испустит значения (все? или только последнее?), которые ранее были переданы субъекту
  • … Ничего не происходит! Фактически, Subject не имеет памяти
const subject$ = new Subject();
// somewhere else in our app
subject.next(/* value */);
// somewhere in our app
subject$.subscribe(/* do something */);
// nothing happening

Это одна из ситуаций, когда ReplaySubject может нам помочь: фактически, Subject будет записывать выданные значения и передавать наблюдателю все значения, выданные при подписке.

Давайте вернемся к вопросу выше: воспроизводит ли ReplaySubject все выбросы или только последнее?

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

const subject$ = new ReplaySubject(1);
subject$.next(1);
subject$.next(2);
subject$.next(3);
subject$.subscribe(console.log);
// Output
// 3

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

const subject$ = new ReplaySubject(100, 250);
setTimeout(() => subject$.next(1), 50);
setTimeout(() => subject$.next(2), 100);
setTimeout(() => subject$.next(3), 150);
setTimeout(() => subject$.next(4), 200);
setTimeout(() => subject$.next(5), 250);
setTimeout(() => {
  subject$.subscribe(v => console.log('SUBCRIPTION A', v));
}, 200);
setTimeout(() => {
  subject$.subscribe(v => console.log('SUBCRIPTION B', v));
}, 400);
  • мы создаем ReplaySubject, у которого bufferSize равно 100 и windowTime 250
  • мы выдаем 5 значений каждые 50 мс
  • мы подписываемся первый раз через 200 мс, а второй раз через 400 мс

Давайте проанализируем результат:

SUBCRIPTION A 1
SUBCRIPTION A 2
SUBCRIPTION A 3
SUBCRIPTION A 4
SUBCRIPTION A 5
SUBCRIPTION B 4
SUBCRIPTION B 5

Подписка A смогла воспроизвести все элементы, но подписка B смогла воспроизвести только элементы 4 и 5, так как они были единственными, выпущенными в течение указанного времени окна.

BehaviorSubject

BehaviorSubject, вероятно, самый известный подкласс Subject. Этот тип Subject представляет «текущее значение».

Интересно, что фреймворк Combine назвал его CurrentValueSubject

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

Чтобы использовать BehaviorSubject, нам нужно предоставить обязательное начальное значение при его создании.

const subject$ = new BehaviorSubject(0); // 0 is the initial value
subject$.next(1);
setTimeout(() => {
  subject$.subscribe(console.log);
}, 200);
// 1

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

Заключительные слова

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

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

Если вам нужны какие-либо разъяснения, или если вы считаете, что что-то неясно или неправильно, оставьте, пожалуйста, комментарий!

Надеюсь, вам понравилась эта статья! Если да, подпишитесь на меня в Medium, Twitter или на моем веб-сайте, чтобы увидеть больше статей о разработке программного обеспечения, Front End, RxJS, Typescript и многом другом!

Учить больше