JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Операторы RxJS: map, filter, tap, debounceTime, switchMap...

Операторы RxJS: map, filter, tap, debounceTime, switchMap

Модуль 4: Node.js, Next.js и Angular
17 уровень , 7 лекция
Открыта

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-запросов, убедитесь, что сервер корректно обрабатывает отмену старых запросов (например, не держит соединения открытыми).

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ