Subject vs BehaviorSubject в TypeScript: главные отличия

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Subject и BehaviorSubject: два брата-акробата в мире реактивного программирования

Если вы когда-нибудь заглядывали на Stack Overflow в обсуждение «What is the difference between Subject and BehaviorSubject?», то знаете: эта тема собирает тысячи просмотров. И неудивительно — это один из тех вопросов, который рано или поздно встаёт перед каждым разработчиком, работающим с RxJS в TypeScript.

Представьте, что вы управляете живой трансляцией данных. Subject — это как открытый микрофон: кто первый подписался, тот и получит всё, что будет сказано после. А BehaviorSubject — это микрофон, который всегда хранит последнюю фразу и сразу же повторяет её новому слушателю. Чувствуете разницу?

В этой статье мы не просто перескажем документацию. Мы разберём реальные кейсы, покажем примеры кода на TypeScript и дадим чёткие правила выбора. Поехали!

Что такое Subject в TypeScript?

Subject — это особый тип Observable, который одновременно является и наблюдателем (Observer). Он может получать данные через метод next() и рассылать их всем подписчикам. Но есть важный нюанс: Subject не хранит предыдущие значения.

Простой пример на TypeScript:

import { Subject } from 'rxjs';

const subject = new Subject<string>();

// Подписываемся после того, как данные уже отправлены
subject.next('Первое сообщение');
subject.next('Второе сообщение');

// Этот подписчик ничего не получит
subject.subscribe({
  next: (value) => console.log('Подписчик:', value)
});

// А это уже будет видно
subject.next('Третье сообщение');

Что выведется в консоль? Только «Подписчик: Третье сообщение». Первые два сообщения потеряны навсегда для этого подписчика. Этот эффект называется «холодным стартом» — вы получаете только те данные, которые были отправлены после подписки.

BehaviorSubject: когда нужно помнить прошлое

BehaviorSubject — это тот же Subject, но с суперспособностью: он всегда хранит последнее значение и выдаёт его новому подписчику сразу при подписке. Обязательное условие — нужно указать начальное значение.

import { BehaviorSubject } from 'rxjs';

const behaviorSubject = new BehaviorSubject<string>('Начальное значение');

// Подписываемся — сразу получаем последнее значение
behaviorSubject.subscribe({
  next: (value) => console.log('Первый подписчик:', value)
});

behaviorSubject.next('Первое обновление');
behaviorSubject.next('Второе обновление');

// Этот подписчик получит 'Второе обновление' сразу
behaviorSubject.subscribe({
  next: (value) => console.log('Второй подписчик:', value)
});

Вывод в консоль:

Первый подписчик: Начальное значение
Первый подписчик: Первое обновление
Первый подписчик: Второе обновление
Второй подписчик: Второе обновление

Видите? Второй подписчик сразу получил последнее значение, хотя подписался позже. Это ключевое отличие BehaviorSubject от Subject.

Сравнение Subject и BehaviorSubject: таблица отличий

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

  • Начальное значение: Subject — нет. BehaviorSubject — обязательно требуется.
  • Хранение последнего значения: Subject — не хранит. BehaviorSubject — хранит и выдаёт новому подписчику.
  • Поведение при поздней подписке: Subject — пропускает все предыдущие события. BehaviorSubject — выдаёт последнее значение.
  • Использование с Angular: Subject — для событий (клики, таймеры). BehaviorSubject — для состояния (данные пользователя, настройки).
  • Метод getValue(): Subject — отсутствует. BehaviorSubject — есть, возвращает текущее значение синхронно.

Когда использовать Subject, а когда BehaviorSubject?

Это самый частый вопрос, который задают на Stack Overflow. Ответ прост: всё зависит от контекста.

Сценарии для Subject

  • Обработка событий (клики, скроллы, нажатия клавиш). Вам не нужно предыдущее событие.
  • Временные уведомления (toast, snackbar). Пользователь не должен видеть старые уведомления.
  • Поток данных, где важен порядок, а не последнее значение (например, логи).

Пример на TypeScript для обработки кликов:

const clickSubject = new Subject<MouseEvent>();

button.addEventListener('click', (event) => {
  clickSubject.next(event);
});

clickSubject.subscribe((event) => {
  console.log('Клик в координатах:', event.clientX, event.clientY);
});

Сценарии для BehaviorSubject

  • Управление состоянием приложения (текущий пользователь, тема, корзина товаров).
  • Формы и ввод данных, где нужно знать последнее значение при повторной подписке.
  • Реактивные сервисы в Angular, где компоненты подписываются в разное время.

Пример сервиса для хранения состояния пользователя:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();

  setUser(user: User) {
    this.userSubject.next(user);
  }

  get currentUser(): User | null {
    return this.userSubject.getValue();
  }
}

Обратите внимание: мы используем asObservable(), чтобы скрыть метод next() от внешнего кода. Это хорошая практика — не давать компонентам напрямую изменять состояние.

Технические тонкости: что важно знать

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

Потокобезопасность и синхронность

И Subject, и BehaviorSubject работают синхронно внутри одного потока. Это значит, что при вызове next() все подписчики получат значение до того, как выполнится следующая строка кода. Но будьте осторожны с асинхронными операциями внутри подписчиков — они могут нарушить порядок.

Метод getValue() у BehaviorSubject

Это мощный инструмент, который позволяет получить текущее значение без подписки. Однако злоупотреблять им не стоит — это нарушает реактивный подход. Используйте getValue() только для синхронных проверок, например, при инициализации.

const counter = new BehaviorSubject<number>(0);
counter.next(5);
console.log(counter.getValue()); // 5

ReplaySubject — золотая середина

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

import { ReplaySubject } from 'rxjs';

const replaySubject = new ReplaySubject<number>(2);
replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);

// Подписчик получит 2 и 3
replaySubject.subscribe(value => console.log(value));

Блоги

Рекомендаций по книгам