Что такое дебаунсер?

Дебаунсер - это функция-обертка, которая ограничивает число выполнений переданной в нее функции, некоторым промежутком времени.

Практическое применение

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

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

Таким образом, запрос не будет совершаться при вводе каждой следующей буквы, а будет ограничен некоторым промежутком времени.

Как реализовать дебаунсер?

Чтобы правильно реализовать дебаунсер, нам нужно чтобы каждый следующий вызов целевой функции “знал” о своем предыдущем вызове, и относительно этих данных дебаунсер решал - выполнять функцию или откладывать.

Для дебаунсера “эти данные” - это идентификатор таймаута.

Так причем тут замыкания?

А при том, что для того, чтобы последующий вызов мог получать доступ к идентификатору таймаута, при этом ограничив этот идентификатор для внешнего изменения удобно использовать замыкание.

Что такое замыкание?

Замыкание - это функция, которая инкапсулирует и возвращает функцию с ее окружением.

Совсем упрощая, можно сказать, что окружение - или лексическое окружение - это блок кода внутри фигурных скобок.

Переменные же, объявленные в этом окружении (в частности в замыкании), во первых, будут недоступны извне этого окружения. Это свойство языка javascript - доступ к переменным окружения есть только у него самого и у его дочерних окружений, при условии, что доступ запрашивается после объявления.

Второе свойство - окружение функции не удаляется из памяти сразу после выполнения самой функции. Таким образом, переменные объявленные в замыкании могут быть “совместно” использованы между разными вызовами возвращаемой замыканием функции.

Давайте теперь уже посмотрим, как оно будет на практике.

Для начала объявим функцию-замыкание debounce, принимающая функцию-колбек и лимит ее выполнения. Она будет содержать переменную хранящую айдишник таймаута каждого последующего вызова:

export const debounce = (callback, interval = 0) => {
  let prevTimeoutId;

  return (...args) => {
    prevTimeoutId = setTimeout(() => {
      callback(args);
    }, interval);
  }
}

Аргументы возвращаемой функции мы передаем в колбек. Обратите внимание - в каком порядке передаются аргументы в замыкание: …args - это те аргументы, которые функция получит на последнем (то есть втором) вызове - например это может быть объект события, если, скажем, дебаунсер передается как обработчик события.

Дальше логика такая: на следующем вызове функции нам нужно удалить из памяти предыдущий вызов - это можно сделать по айди таймаута с помощью функции clearTimeout. Затем нам нужно объявить новый таймаут, сохранить его айдишник и вернуть новую функцию:

const debounce = (callback, interval = 0) => {
  let prevTimeoutId;
 
  return (...args) => {
    clearTimeout(prevTimeoutId);
    prevTimeoutId = setTimeout(() => callback(...args), interval);
  }
}

Теперь, если мы захотим теперь использовать наш дебаунсер на инпуте, то выглядеть это будет так:

document.querySelector('input').addEventListener(
  'input',
  debounce(ev => console.log(ev.target.value), 1000)
);

И при вводе символов в инпут, выводится в консоль будут только значения, введенные в интервале одной секунды.

Добавим реактивности!

В случае, если мы хотим использовать дебаунсер внутри реакт-компонента, то его нужно преобразовать в кастомный реакт-хук, чтобы идентификатор предыдущего таймаута и возвращаемая функция не исчезали из памяти. Делается это при помощи хуков useRef и useCallback соответственно:

const useDebounce = (callback, interval = 0) => {
  const prevTimeoutIdRef = React.useRef();

  return React.useCallback(
    (...args) => {
      clearTimeout(prevTimeoutIdRef.current);
      prevTimeoutIdRef.current = setTimeout(() => {
        clearTimeout(prevTimeoutIdRef.current);
        callback(...args);
      }, interval);
    },
    [callback, interval]
  );
};

И если мы дальше планируем использовать его в useEffect, то во избежании ошибок сначала нужно инициализировать useDebounce с колбеком и интервалом в переменную, а уже потом вызывать эту переменную в useEffect и передавать в нее нужные аргументы.

На этом собственно все.

Надеюсь вам понравилось это увлекательное путешествие в мир дебаунсеров и замыканий.

Рекомендую следующую статью по замыканиям:

https://learn.javascript.ru/closure

спасибо

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


  1. dyadyaSerezha
    24.06.2023 13:18
    +1

    clearTimeout(timeoutId);

    timeoutId - не та переменная.


    1. karmacan Автор
      24.06.2023 13:18

      спасибо, поправил!


  1. dopusteam
    24.06.2023 13:18
    +2

    но чтобы не грузить сервер запросами и не повторять рендер страницы с одинаковыми по значению данными, мы хотим обернуть запрос в дебаунсер с некоторым ограничением по времени

    Кажется дебаунсер тут не поможет


    1. karmacan Автор
      24.06.2023 13:18

      в контексте отправки запросов при вводе в инпут - поможет )
      вы уменьшаете количество отправляемых запросов - таким образом уменьшаете нагрузку на сервер,
      а обрабатывая меньшее количество ответов - уменьшаете количество обновлений вашего компонента.
      второе в целом не очень существенно прибавляет в производительности, если вы правильно мемоизируете компонент, который вы рендерите. но если представить, что у вас много таких компонентов на странице, я посчитал этот аргумент валидным

      о том как дебаунсер увеличивает производительность ui можете посмотреть здесь:
      https://css-tricks.com/debouncing-throttling-explained-examples/#aa-debounce


  1. BigDflz
    24.06.2023 13:18
    -3

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

    для данного применения достаточно просто ограничить первую отправку на сервер 3-4 символами. и реализовать разделение на группы пробелом, тогда на сервере логика выбора будет поле like %1группа% and поле like %2группа% и так далее. как правило редко приходится вбивать более 3-х групп для выборки аз 10 лямов записей - из практики применения. замыкания, конечно хорошо надо знать, но и применять надо по месту.


  1. deamondz
    24.06.2023 13:18
    +1

    а зачем это всё, если есть https://react.dev/reference/react/useDeferredValue?


    1. vagon333
      24.06.2023 13:18

      Если использовать не только в React, то вполне полезная функция.

      Кстати, функция легко гуглится "function debounce(f, ms)", выдает компактный код и внятное объяснение.

      Использую для сохранения последней выбранной записи, когда пользователь бегает по гриду.
      Если сохранять каждый раз при переходе на запись - перегруз запросов. А если с задержкой в 2сек поесле последнего перехода - самое то.


    1. DmitryKoterov
      24.06.2023 13:18

      useDererredValue() же не позволяет задавать задержку.


  1. nin-jin
    24.06.2023 13:18
    -1

    А вот так дебонс выглядит в $mol:

    @act someAction() {
      sleep( 1000 )
      // do some work
    }

    Нет, не продаю, просто показываю.


    1. dopusteam
      24.06.2023 13:18

      А что из этого debounce? Act? Sleep?


      1. nin-jin
        24.06.2023 13:18

        В том-то и дело, что он не нужен. sleep вставляет задержку. Если тот же экшен вызывается до завершения предыдущего, то предыдущий прибивается автоматически.


        1. dopusteam
          24.06.2023 13:18

          А как sleep понимает где он вызван? У меня ж там разные могут быть методы со sleep


          1. nin-jin
            24.06.2023 13:18

            Так же как и хуки в реакте.


    1. shasoftX
      24.06.2023 13:18

      Для лучшего понимания тут бы наверное стоило или в таком виде задавать

      @act 1000 someAction() {
        // do some work
      }

      или вот в таком

      @act someAction() {
        debonce( 1000 )
        // do some work
      }

      Иначе, ИМХО, стороннему человеку этот код реально кажется непонятным


      1. nin-jin
        24.06.2023 13:18

        Любая достаточно развитая технология неотличима от магии. Тут про это рассказывается: https://mol.hyoo.ru/#!section=docs/=97b2fn_qb9le1