Внедрение зависимостей и RxJS — Примеры
Пример 1 — Поиск, сохраняющий запрос на каждое нажатие
Поле поиска отображает свой ввод в запрос, и результаты иногда принадлежат более раннему запросу. Скажите, какой оператор оставляет старые запросы живыми, и отмените их.
readonly results$ = this.query$.pipe(
debounceTime(250),
mergeMap((q) => this.http.get<Result[]>(`/api/search?q=${encodeURIComponent(q)}`)),
);
Решение
readonly results$ = this.query$.pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap((q) => this.http.get<Result[]>(`/api/search?q=${encodeURIComponent(q)}`)),
catchError(() => of([])),
);
Observable — не промис, а поток: промис разрешается один раз, а observable может испустить ноль, одно или много значений. Операторы описывают, как один поток значений становится другим: mergeMap оставляет работать каждый внутренний observable, поэтому медленный ранний запрос остаётся жив и может испустить значение после более быстрого позднего. switchMap отписывается от предыдущего внутреннего observable, когда приходит новое значение, и это ровно та отмена, которая нужна полю поиска; именно поэтому результаты всегда принадлежат текущему запросу. debounceTime вообще сокращает число запускаемых запросов, а distinctUntilChanged не даёт повторному значению запустить ту же работу заново. Читайте цепочку операторов слева направо как путь, который проходит значение, — так конвейеры и становятся понятными.
Пример 2 — Подписка, переживающая свой компонент
Компонент подписывается на сервис в конструкторе и никогда не останавливается. Возвращение на экран и уход с него снова накапливают слушателей. Скажите, что удерживает компонент живым, и позвольте фреймворку управлять подпиской.
export class OrdersComponent implements OnInit {
orders: Order[] = [];
constructor(private readonly ordersService: OrdersService) {
this.ordersService.list().subscribe((orders) => (this.orders = orders));
}
}
Решение
export class OrdersComponent {
readonly orders$ = inject(OrdersService).list();
}
@if (orders$ | async; as orders) {
@for (order of orders; track order.id) { <li>{{ order.id }}</li> }
}
Ручная подписка держит ссылку на компонент столько же, сколько живёт поток, поэтому каждый визит на экран добавляет ещё одного слушателя, а освободить компонент нельзя: симптом — устаревшие данные, приходящие после того, как пользователь уже ушёл. Пайп async подписывается, когда шаблон отрисовывается, и отписывается, когда его уничтожают, что полностью убирает бухгалтерию в ngOnDestroy, а там, где подписка обязана быть ручной, ту же работу делает takeUntilDestroyed. Подписка в конструкторе — вторая ошибка, которую нужно заметить: она выполняется до инициализации компонента и до того, как задан хоть один вход, поэтому работа, которая должна произойти один раз, принадлежит ngOnInit, а её очистка — ngOnDestroy.