9. debounce и throttle
⏱️ Управление временем: debounce, throttle и друзья
Заголовок раздела «⏱️ Управление временем: debounce, throttle и друзья»Яша, время — деньги! ⏰ Сегодня учимся контролировать, как часто наш код реагирует на события. Это один из самых практичных уроков — ты будешь использовать эти операторы каждый день!
🧠 Зачем ограничивать частоту?
Заголовок раздела «🧠 Зачем ограничивать частоту?»Представь: пользователь печатает в поисковой строке. Каждое нажатие клавиши — событие. Если на каждое нажатие отправлять запрос на сервер… 😱
- Слово “javascript” = 10 запросов
- 1000 пользователей = 10 000 запросов на одно слово
Нам нужно умно фильтровать события по времени!
🛗 debounceTime — лифт с удерживаемой дверью
Заголовок раздела «🛗 debounceTime — лифт с удерживаемой дверью»debounceTime — это как кнопка закрытия двери лифта: нажал ещё раз — таймер сбрасывается. Лифт поедет только когда перестанешь нажимать (и пройдёт указанное время).
Мраморная диаграмма (debounceTime(300)):
События: --a-b-c--------d-e--| 300мс ↕ 300мс ↕debounce: -----------c------e-|import { fromEvent } from 'rxjs';import { debounceTime, map } from 'rxjs/operators';
const searchInput = document.getElementById('search') as HTMLInputElement;
// Без debounce: запрос на КАЖДОЕ нажатиеfromEvent(searchInput, 'input').subscribe(() => { // 🔴 "j" → запрос // 🔴 "ja" → запрос // 🔴 "jav" → запрос // ... 10 запросов для слова "javascript"});
// С debounceTime(300): запрос только после паузы 300мсfromEvent(searchInput, 'input').pipe( debounceTime(300), map(e => (e.target as HTMLInputElement).value),).subscribe(query => { // ✅ Только один запрос после паузы в печатании! fetch(`/api/search?q=${query}`);});💡 Правило большого пальца:
debounceTime(300)для поиска,debounceTime(500)для менее срочных операций.
💓 throttleTime — сердцебиение
Заголовок раздела «💓 throttleTime — сердцебиение»throttleTime — это кардиостимулятор: пульс не чаще одного раза в X миллисекунд. Первое событие проходит сразу, остальные в течение интервала — отбрасываются.
Мраморная диаграмма (throttleTime(500)):
События: --a-b-c-d-e----f-g-h--> |--- 500мс ---|--- 500мс ---throttle: --a-----------f--------->import { fromEvent } from 'rxjs';import { throttleTime, map } from 'rxjs/operators';
// Отслеживание позиции мышиfromEvent(document, 'mousemove').pipe( throttleTime(100), // не чаще 10 раз в секунду map(e => ({ x: (e as MouseEvent).clientX, y: (e as MouseEvent).clientY }))).subscribe(pos => { updateCursorPosition(pos); // вместо 1000 вызовов/сек → 10});
// Защита от спама кнопкиconst btn = document.getElementById('action-btn')!;fromEvent(btn, 'click').pipe( throttleTime(1000) // не чаще 1 раза в секунду).subscribe(() => { doSomething();});🔬 debounce vs throttle — в чём разница?
Заголовок раздела «🔬 debounce vs throttle — в чём разница?»Пользователь печатает: j-a-v-a-s-c-r-i-p-t (за 1 секунду)↕ ↕debounceTime(300) throttleTime(300) Ждёт паузу Первое + периодически Эмитит ПОСЛЕ Эмитит СРАЗУ
debounce: ---------------------------javascript (ждёт 300мс после последнего нажатия)
throttle: --j-----------s-----------t (первый символ, потом через каждые 300мс)| debounceTime | throttleTime | |
|---|---|---|
| Эмитит | После паузы | Сразу + периодически |
| Последнее значение | ✅ Всегда | ❌ Может потерять |
| Идеально для | Поиск, валидация | Скролл, мышь, аналитика |
📊 auditTime — итог за период
Заголовок раздела «📊 auditTime — итог за период»auditTime ждёт X миллисекунд, затем эмитит последнее значение за этот период. Похож на дебаунс, но таймер не сбрасывается.
Мраморная диаграмма (auditTime(500)):
События: --a-b-c---d-e------f--> |-- 500мс --|-- 500мс --|audit: ----------e-----------f-->import { fromEvent } from 'rxjs';import { auditTime, map } from 'rxjs/operators';
// Позиция скролла: отправляем аналитику не чаще раза в секунду,// но берём ПОСЛЕДНЮЮ позициюfromEvent(window, 'scroll').pipe( auditTime(1000), map(() => window.scrollY)).subscribe(position => { sendAnalytics({ scrollDepth: position });});🎯 sampleTime — снимок по таймеру
Заголовок раздела «🎯 sampleTime — снимок по таймеру»sampleTime делает “снимок” последнего значения через каждые X миллисекунд, независимо от того, было ли новое событие.
import { interval, Subject } from 'rxjs';import { sampleTime } from 'rxjs/operators';
// Мониторинг производительности: снимаем метрики каждую секундуconst fps$ = new Subject<number>();
fps$.pipe( sampleTime(1000) // снимок каждую секунду).subscribe(currentFps => { console.log(`FPS: ${currentFps}`);});🏆 Реальный пример: форма с умной валидацией
Заголовок раздела «🏆 Реальный пример: форма с умной валидацией»import { fromEvent, combineLatest } from 'rxjs';import { debounceTime, map, distinctUntilChanged, startWith } from 'rxjs/operators';
const emailInput = document.getElementById('email') as HTMLInputElement;const passwordInput = document.getElementById('password') as HTMLInputElement;
const email$ = fromEvent(emailInput, 'input').pipe( debounceTime(500), map(e => (e.target as HTMLInputElement).value), distinctUntilChanged(), startWith(''));
const password$ = fromEvent(passwordInput, 'input').pipe( debounceTime(300), map(e => (e.target as HTMLInputElement).value), startWith(''));
// Валидируем форму только после паузы в вводеcombineLatest([email$, password$]).pipe( map(([email, password]) => ({ emailValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email), passwordValid: password.length >= 8, canSubmit: email.length > 0 && password.length > 0, }))).subscribe(({ emailValid, passwordValid, canSubmit }) => { console.log({ emailValid, passwordValid, canSubmit }); // Обновляем UI только когда пользователь сделал паузу});Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: