1. Оператор map: трансформация данных
Работа с Observable без операторов — как попытка нарезать салат, не имея ножа. Операторы — это инструменты, которые позволяют вам "готовить" данные прямо в потоке: преобразовывать, фильтровать, задерживать, комбинировать и даже отменять ненужные запросы.
В мире RxJS оператор — это просто функция, которую вы вызываете внутри метода pipe(). Операторы бывают разные: одни трансформируют значения (map), другие фильтруют (filter), третьи позволяют делать побочные действия (tap), а некоторые — управляют временем или асинхронностью (debounceTime, switchMap).
import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';
of(1, 2, 3, 4, 5)
.pipe(
filter(n => n % 2 === 0), // пропускаем только чётные
map(n => n * 10) // умножаем на 10
)
.subscribe(console.log); // → 20, 40
map — это оператор, который позволяет преобразовать каждое значение, проходящее через Observable, в новое значение. Он очень похож на одноимённый метод массива JavaScript.
Синтаксис
import { map } from 'rxjs/operators';
observable$.pipe(
map(value => /* преобразование */)
)
Пример 1: Преобразование чисел
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
of(1, 2, 3)
.pipe(
map(n => n * 2)
)
.subscribe(console.log); // → 2, 4, 6
Пример 2: Преобразование ответа от сервера
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
Но нам нужен только email:
this.userService.getUser(1)
.pipe(
map(user => user.email)
)
.subscribe(email => {
console.log('Email:', email);
});
Аналогия: map — это как "фильтр" в Instagram: исходное фото (значение) остаётся, но вы его быстро преобразуете по своему вкусу.
2. Оператор filter: фильтрация потока
filter пропускает только те значения, которые удовлетворяют вашему условию. Всё остальное — игнорирует.
Синтаксис
import { filter } from 'rxjs/operators';
observable$.pipe(
filter(value => /* условие */)
)
Пример 1: Фильтрация чисел
of(1, 2, 3, 4, 5, 6)
.pipe(
filter(n => n % 2 === 0)
)
.subscribe(console.log); // → 2, 4, 6
Пример 2: Фильтрация объектов
of(
{ name: 'Alice', active: true },
{ name: 'Bob', active: false }
).pipe(
filter(user => user.active)
)
.subscribe(console.log); // → { name: 'Alice', active: true }
Аналогия: filter — как сито: высыпаете туда всё, а на выходе только то, что подходит по размеру.
3. Оператор tap: побочные эффекты (side effects)
tap (раньше назывался do) позволяет выполнять побочные действия для каждого значения в потоке, не изменяя сами значения. Используется, например, для логирования, отображения загрузки, вызова методов, которые не должны влиять на поток.
Синтаксис
import { tap } from 'rxjs/operators';
observable$.pipe(
tap(value => {
// побочный эффект, например, логирование
console.log('Получено значение:', value);
})
)
Пример: Логирование и спиннеры
this.http.get('/api/data')
.pipe(
tap(() => this.loading = true),
map(data => processData(data)),
tap(() => this.loading = false)
)
.subscribe();
Аналогия: tap — как шпион: наблюдает за потоком, но не вмешивается в него.
4. Оператор debounceTime: защита от "дребезга" событий
debounceTime задерживает выдачу значения на заданное время. Если за это время приходит новое значение — старое игнорируется. Очень полезно для обработки частых событий (например, ввод текста пользователем).
Синтаксис
import { debounceTime } from 'rxjs/operators';
observable$.pipe(
debounceTime(300) // 300 миллисекунд
)
Пример: Поиск при вводе
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
const searchBox = document.getElementById('search') as HTMLInputElement;
fromEvent(searchBox, 'input')
.pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300)
)
.subscribe(value => {
console.log('Запрос к серверу с:', value);
// Здесь можно вызывать this.http.get(...)
});
Аналогия: debounceTime — как "антиспам": не реагирует на каждое нажатие, а ждёт, пока пользователь не перестанет стучать по клавиатуре.
5. Оператор switchMap: отмена и переключение потоков
switchMap — это оператор для работы с вложенными потоками (Observable в Observable). Он отменяет предыдущий внутренний Observable, если приходит новое значение. Это особенно полезно для сценариев типа "поиск по мере ввода": если пользователь быстро меняет запрос, не нужно ждать ответа на старые запросы — только на последний.
Синтаксис
import { switchMap } from 'rxjs/operators';
observable$.pipe(
switchMap(value => {
// возвращаем новый Observable
return другойObservable$(value);
})
)
Пример: Поиск с отменой старых запросов
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
// Представим, что у нас есть сервис поиска
search(term: string): Observable<Result[]> {
return this.http.get<Result[]>(`/api/search?q=${term}`);
}
// В компоненте:
fromEvent(searchBox, 'input').pipe(
map(event => (event.target as HTMLInputElement).value),
debounceTime(300),
switchMap(term => this.search(term))
)
.subscribe(results => {
// Отобразить результаты поиска
});
Важно! Если пользователь быстро печатает, switchMap автоматически отменит все старые HTTP-запросы и обработает только последний.
Аналогия: switchMap — как официант, который берет только последний заказ клиента, а все предыдущие забывает.
6. Пример: Реактивный поиск в Angular-компоненте
Давайте свяжем всё вместе и реализуем реальный пример — поле поиска, которое отправляет запросы к серверу только после паузы в наборе текста и всегда показывает только актуальный результат.
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { debounceTime, switchMap, tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-search',
template: `
<input [formControl]="searchControl" placeholder="Поиск...">
<div *ngIf="loading">Загрузка...</div>
<ul>
<li *ngFor="let item of results$ | async">{{ item.name }}</li>
</ul>
`
})
export class SearchComponent implements OnInit {
searchControl = new FormControl('');
results$: Observable<any[]>;
loading = false;
constructor(private http: HttpClient) {}
ngOnInit() {
this.results$ = this.searchControl.valueChanges.pipe(
debounceTime(400),
tap(() => this.loading = true),
switchMap(term =>
term
? this.http.get<any[]>(`/api/search?q=${term}`)
: of([])
),
tap(() => this.loading = false)
);
}
}
Что тут происходит:
- debounceTime(400) — ждём 400 мс после последнего ввода.
- tap — показываем/скрываем индикатор загрузки.
- switchMap — отменяем старые запросы, если пользователь продолжает вводить текст.
- Если поле пустое, возвращаем пустой массив (of([])), чтобы не делать лишних запросов.
7. Практика: фильтрация и преобразование данных из API
Допустим, у вас есть массив пользователей с сервера, и вы хотите отобразить только активных, а их имена сделать заглавными.
this.http.get<User[]>('/api/users').pipe(
map(users => users.filter(user => user.active)), // фильтруем по активности
map(users => users.map(user => ({
...user,
name: user.name.toUpperCase()
})))
)
.subscribe(activeUsers => {
console.log(activeUsers);
});
8. Типичные ошибки при использовании операторов RxJS
Ошибка №1: забыли про отписку.
Если вы подписываетесь на Observable, который не завершится сам (например, события DOM или valueChanges формы), обязательно отписывайтесь в ngOnDestroy или используйте async pipe в шаблоне. Иначе — утечки памяти.
Ошибка №2: перепутали map и switchMap.
map просто меняет данные, а switchMap нужен, когда внутри возвращаете новый Observable (например, для HTTP-запроса). Если внутри map вы делаете HTTP-запрос, но не используете switchMap, получите Observable внутри Observable, и ничего не сработает.
Ошибка №3: забыли про debounceTime в поиске.
Если не использовать debounceTime, каждый ввод символа будет отправлять HTTP-запрос — серверу может стать плохо.
Ошибка №4: побочные действия внутри map.
Не используйте map для побочных эффектов (логирование, изменение состояния компонента) — для этого есть tap.
Ошибка №5: неправильная работа с асинхронными потоками.
Если вы используете switchMap для HTTP-запросов, убедитесь, что сервер корректно обрабатывает отмену старых запросов (например, не держит соединения открытыми).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ