Цель

В этой статье я хочу рассказать тонкую разницу между операторами debounceTime и throttleTime простыми словами


Общее

1) У нас есть метод в сервисе нашего Angular приложения

getData(id: number): Observable<RxjsResponse> {
  return this.httpClient.get<RxjsResponse>(`${this.basePath}/data/${id}`);
}

Это простой метод, который обращается на backend и получает ответ в виде

{
  id: number;
  name: string;
}

2) Есть backend нашего приложения

justData.get('/data/:id', async (req: Request, res: Response): Promise<void> => {
    const id = req.params.id;

    if (id === '1') {
        res.status(200).send({
            id: 1,
            name: 'Первый'
        });
    }
    if (id === '2') {
        res.status(200).send({
            id: 2,
            name: 'Второй'
        });
    }
    if (id === '3') {
        res.status(200).send({
            id: 3,
            name: 'Третий'
        });
    }
    if (id === '4') {
        res.status(200).send({
            id: 4,
            name: 'Четвёртый'
        });
    }
});

Это простой роут на бэке, который в зависимости от id возвращает определённый ответ


debounceTime

(Ну что, успокоился?)

Посмотрим как работает debounceTime, он проще для понимания

interval(1000)
  .pipe(
    take(5),
    filter((id) => id !== 0),
    debounceTime(1500),
    mergeMap((id) => this.rxjsService.getData(id)),
    tap((result) => console.log(result))
  )
  .subscribe();

Что тут происходит?

  • interval выкидывает целое число (0, 1, 2, 3......) каждую секунду

  • take(5) отпишется от interval после пяти эмитов (до следующего оператора filter дойдут только числа от 0 до 4)

  • filter((id) => id !== 0) не пропустит первый эмит (до следующего оператора debounceTime дойдут только idшки от 1 до 4)

А теперь рассмотрим что происходит после фильтра.

Оператор debounceTime не пропустит ни один эмит, к mergeMap, пока после предыдущего эмита не пройдёт то кол-во времени, которое указано в операторе. То есть в данном случае, когда единица дойдёт до debounceTime мы должны будем подождать 1.5 секунды, чтобы пройти с ней к mergeMap, но, так как у нас эмиты происходят каждую секунду, единица так и не пройдёт дальше, придёт двойка, поэтому про единицу можно забыть и так далее до конца. Догадываетесь какой будет результат?

Алгоритм:

  • Эмит 0 -> filter не пропустил

  • Эмит 1 -> id = 1 дошла до debounceTime, ждём полторы секунды

  • Эмит 2 -> id = 2 сэмитилась быстрее, чем время ожидания от debounceTime, забываем про id = 1, ждём 1.5 секунды с id = 2

  • Эмит 3 -> id = 3 сэмитилась быстрее, чем время ожидания от debounceTime, забываем про id = 2, ждём 1.5 секунды с id = 3

  • Эмит 4 -> id = 4 сэмитилась быстрее, чем время ожидания от debounceTime, забываем про id = 3, ждём 1.5 секунды

  • Больше эмитов нет, проходит 1.5 секунды, id = 4 двигается к mergeMap, делаем запрос на сервер

// Ответ
{
  "id": 4,
  "name": "Четвёртый"
}

Самый банальный пример использования: у вас есть input, который отвечает за поиск на сервере и отсылает запрос при изменении значения в input. Пользователь каждую секунду в этот поиск вносит новый символ. Зачем вам реагировать на каждое изменение и грузить сервер, когда можно отмести все лишние значения и уже когда пользователь перестанет вводить символы, пройдёт n секунд, уже пустить запрос на сервер?


throttleTime

(Открыть шлюз! Закрыть шлюз!)

interval(1000)
  .pipe(
    take(5),
    filter((id) => id !== 0),
    throttleTime(1900),
    mergeMap((id) => this.rxjsService.getData(id)),
    tap((result) => console.log(result))
  )
  .subscribe();

Код тот же самый, что и в предыдущем примере, только теперь у нас throttleTime(1900).

Оператор throttleTime, после первого пройденного через него эмита, не будет пропускать следующие эмиты в течении того количества времени, которое в нём указано. То есть в данном случае, когда единица пройдёт через throttleTime, клапан закроется на 1.9 секунд и, например, следующий эмит, который произойдёт через секунду к mergeMap не пройдёт, а третий уже пройдёт, потому что он будет сделан через 2 секунды, клапан к тому времени уже будет открыт, а потом снова закроется на 1.9 секунды

Алгоритм:

  • Эмит 0 -> filter не пропустил

  • Эмит 1 -> id = 1 прошёл через throttleTime, клапан закрылся, пошёл запрос на сервер

  • Эмит 2 -> id = 2 доходит до throttleTime, клапан закрыт, не проходит дальше

  • Эмит 3 -> id = 3 доходит до throttleTime, клапан открыт, проходит в mergeMap, клапан закрылся, пошёл запрос на сервер

  • Эмит 4 -> id = 4 доходит до throttleTime, клапан закрыт, не проходит дальше

//Ответ
{
    "id": 1,
    "name": "Первый"
}
{
    "id": 3,
    "name": "Третий"
}

Ещё один банальный абстрактный пример: вам нужно ограничить количество кликов по кнопке. Пользователь кликает по пять раз в секунду, а вам нужно пропускать только один клик в секунду, throttleTime(1000) вам в помощь


Заключение

Для тех, кто хочет сам потыкать на кнопки и посмотреть как работают операторы вот проекты на github. Поизменяйте время в операторах, почувствуйте как это работает

Фронт часть

Бэк часть

Это всё. Надеюсь, статья была для вас полезной.

Спасибо :)

Комментарии (2)


  1. pikus_spb
    00.00.0000 00:00
    +1

    Отлично написано, все стало ясно. Спасибо за пост.