Debouncer: практический пример использования замыкания

в 12:30, , рубрики: javascript, ReactJS, дебаунсер, замыкание

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const debounce = (callback, interval = 0) => {
  let prevTimeoutId;
 
  return (...args) => {
    clearTimeout(timeoutId);
    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 и передавать в нее нужные аргументы.

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

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

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

Автор:
karmacan

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js