5. map, filter, reduce
🔄 Трансформация потоков: map, filter, reduce, scan
Заголовок раздела «🔄 Трансформация потоков: map, filter, reduce, scan»Привет, Яша! 🎉 Сегодня мы разберём самые важные операторы трансформации данных в RxJS. Это твой швейцарский нож — используешь их каждый день!
🧠 Зачем трансформировать потоки?
Заголовок раздела «🧠 Зачем трансформировать потоки?»Представь, что Observable — это конвейерная лента на заводе. Сырые детали едут по ленте, и на каждой станции что-то происходит: одни детали отбраковываются, другие обрабатываются, третьи складываются в стопки. Операторы — это и есть те самые станции! 🏭
Сырые данные → [map] → [filter] → [reduce] → Готовый результат🗺️ map — преобразуй каждый элемент
Заголовок раздела «🗺️ map — преобразуй каждый элемент»map работает точно так же, как Array.prototype.map, только для потоков. Берёт каждое значение и возвращает новое.
Мраморная диаграмма:
Источник: --1----2----3----4-->map(x=>x*2):Результат: --2----4----6----8-->import { of, from } from 'rxjs';import { map } from 'rxjs/operators';
// Простейший примерof(1, 2, 3, 4, 5).pipe( map(x => x * 2)).subscribe(console.log);// 2, 4, 6, 8, 10
// Реальный пример: трансформация ответа APIinterface ApiUser { first_name: string; last_name: string; age: number;}
interface DisplayUser { fullName: string; isAdult: boolean;}
from(fetch('/api/users').then(r => r.json())).pipe( map((users: ApiUser[]) => users.map(u => ({ fullName: `${u.first_name} ${u.last_name}`, isAdult: u.age >= 18, })) )).subscribe(displayUsers => { console.log(displayUsers);});💡 Запомни:
map— чистая функция. Входные данные не меняются, создаётся новое значение.
🔍 filter — пропускай только нужное
Заголовок раздела «🔍 filter — пропускай только нужное»filter работает как охранник на входе: пропускает только тех, кто соответствует условию.
Мраморная диаграмма:
Источник: --1----2----3----4----5-->filter(x => x > 2):Результат: ---------------3----4----5-->import { from } from 'rxjs';import { filter, map } from 'rxjs/operators';
// Фильтрация событий пользователяinterface UserEvent { type: 'click' | 'hover' | 'keypress'; target: string; value?: string;}
const events$ = from<UserEvent[]>([ { type: 'hover', target: 'button' }, { type: 'click', target: 'button' }, { type: 'keypress', target: 'input', value: 'H' }, { type: 'click', target: 'link' },]);
// Оставляем только кликиevents$.pipe( filter(event => event.type === 'click')).subscribe(event => { console.log('Клик по:', event.target);});// "Клик по: button"// "Клик по: link"
// Цепочка filter + mapevents$.pipe( filter(event => event.type === 'keypress'), filter(event => !!event.value), map(event => event.value!.toUpperCase())).subscribe(char => { console.log('Нажата клавиша:', char);});// "Нажата клавиша: H"📦 reduce — собери всё в одно
Заголовок раздела «📦 reduce — собери всё в одно»reduce ждёт завершения потока и накапливает результат — как копилка, которую вскрываешь только в конце. 🐷
Мраморная диаграмма:
Источник: --1----2----3--|reduce((a,b)=>a+b, 0):Результат: ---------------6|⚠️ Важно:
reduceэмитит значение ТОЛЬКО когда поток завершился (complete). Для бесконечных потоков — не подходит!
import { from } from 'rxjs';import { reduce } from 'rxjs/operators';
// Подсчёт суммы заказаinterface OrderItem { name: string; price: number; quantity: number;}
const items: OrderItem[] = [ { name: 'Кофе', price: 150, quantity: 2 }, { name: 'Круассан', price: 80, quantity: 3 }, { name: 'Сок', price: 120, quantity: 1 },];
from(items).pipe( reduce((total, item) => total + item.price * item.quantity, 0)).subscribe(total => { console.log(`Итого: ${total} руб.`); // "Итого: 660 руб."});
// Собираем данные в объектfrom(['Alice', 'Bob', 'Alice', 'Charlie', 'Bob', 'Bob']).pipe( reduce((acc, name) => { acc[name] = (acc[name] || 0) + 1; return acc; }, {} as Record<string, number>)).subscribe(counts => { console.log(counts); // { Alice: 2, Bob: 3, Charlie: 1 }});🌊 scan — reduce в реальном времени
Заголовок раздела «🌊 scan — reduce в реальном времени»scan — это reduce, но который эмитит значение на КАЖДОМ шаге. Как бегущий итог в кассовом аппарате! 🧾
Мраморная диаграмма:
Источник: --1----2----3----4-->scan((a,b)=>a+b, 0):Результат: --1----3----6----10-->import { Subject } from 'rxjs';import { scan, map } from 'rxjs/operators';
// Корзина покупок в реальном времениinterface CartAction { type: 'add' | 'remove'; item: string; price: number;}
const cartActions$ = new Subject<CartAction>();
// Состояние корзины обновляется при каждом действииcartActions$.pipe( scan((cart, action) => { if (action.type === 'add') { return { items: [...cart.items, action.item], total: cart.total + action.price, }; } else { return { items: cart.items.filter(i => i !== action.item), total: cart.total - action.price, }; } }, { items: [] as string[], total: 0 })).subscribe(cart => { console.log('Корзина:', cart.items, 'Сумма:', cart.total);});
cartActions$.next({ type: 'add', item: 'Книга', price: 500 });// Корзина: ['Книга'] Сумма: 500
cartActions$.next({ type: 'add', item: 'Ручка', price: 50 });// Корзина: ['Книга', 'Ручка'] Сумма: 550
cartActions$.next({ type: 'remove', item: 'Книга', price: 500 });// Корзина: ['Ручка'] Сумма: 50💡
scanидеально подходит для управления состоянием в стиле Redux!
🔎 pluck / pick — достань вложенное свойство
Заголовок раздела «🔎 pluck / pick — достань вложенное свойство»pluck устарел в RxJS 8, но его легко заменить через map. Он извлекает вложенное свойство объекта.
import { from } from 'rxjs';import { map } from 'rxjs/operators';
interface Response { data: { user: { name: string; email: string; }; };}
// Вместо pluck используй map:from<Response[]>([]).pipe( map(response => response.data.user.name) // достаём только имя).subscribe(name => console.log(name));// "Alice"// "Bob"🔗 Реальный пример: поисковая строка
Заголовок раздела «🔗 Реальный пример: поисковая строка»Вот как операторы работают вместе в реальном сценарии:
import { fromEvent } from 'rxjs';import { map, filter, debounceTime, distinctUntilChanged } from 'rxjs/operators';
const searchInput = document.getElementById('search') as HTMLInputElement;
fromEvent(searchInput, 'input').pipe( map(event => (event.target as HTMLInputElement).value), filter(value => value.length > 2), // минимум 3 символа debounceTime(300), // ждём паузы distinctUntilChanged(), // не повторяем одинаковые map(value => value.trim().toLowerCase()), // нормализуем).subscribe(query => { console.log('Ищем:', query); // отправляем запрос на сервер});Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: