Что такое дебаунсер?
Дебаунсер - это функция-обертка, которая ограничивает число выполнений переданной в нее функции, некоторым промежутком времени.
Практическое применение
Предположим, при вводе текста в инпут мы хотим отправлять запрос на сервер, чтобы получить выпадающий список вариантов под введенное значение.
По умолчанию, запрос будет уходить при вводе очередной буквы, но чтобы не грузить сервер запросами и не повторять рендер страницы с одинаковыми по значению данными, мы хотим обернуть запрос в дебаунсер с некоторым ограничением по времени.
Таким образом, запрос не будет совершаться при вводе каждой следующей буквы, а будет ограничен некоторым промежутком времени.
Как реализовать дебаунсер?
Чтобы правильно реализовать дебаунсер, нам нужно чтобы каждый следующий вызов целевой функции “знал” о своем предыдущем вызове, и относительно этих данных дебаунсер решал - выполнять функцию или откладывать.
Для дебаунсера “эти данные” - это идентификатор таймаута.
Так причем тут замыкания?
А при том, что для того, чтобы последующий вызов мог получать доступ к идентификатору таймаута, при этом ограничив этот идентификатор для внешнего изменения удобно использовать замыкание.
Что такое замыкание?
Замыкание - это функция, которая инкапсулирует и возвращает функцию с ее окружением.
Совсем упрощая, можно сказать, что окружение - или лексическое окружение - это блок кода внутри фигурных скобок.
Переменные же, объявленные в этом окружении (в частности в замыкании), во первых, будут недоступны извне этого окружения. Это свойство языка 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)
dopusteam
24.06.2023 13:18+2но чтобы не грузить сервер запросами и не повторять рендер страницы с одинаковыми по значению данными, мы хотим обернуть запрос в дебаунсер с некоторым ограничением по времени
Кажется дебаунсер тут не поможет
karmacan Автор
24.06.2023 13:18в контексте отправки запросов при вводе в инпут - поможет )
вы уменьшаете количество отправляемых запросов - таким образом уменьшаете нагрузку на сервер,
а обрабатывая меньшее количество ответов - уменьшаете количество обновлений вашего компонента.
второе в целом не очень существенно прибавляет в производительности, если вы правильно мемоизируете компонент, который вы рендерите. но если представить, что у вас много таких компонентов на странице, я посчитал этот аргумент валидным
о том как дебаунсер увеличивает производительность ui можете посмотреть здесь:
https://css-tricks.com/debouncing-throttling-explained-examples/#aa-debounce
BigDflz
24.06.2023 13:18-3Предположим, при вводе текста в инпут мы хотим отправлять запрос на сервер, чтобы получить выпадающий список вариантов под введенное значение.
для данного применения достаточно просто ограничить первую отправку на сервер 3-4 символами. и реализовать разделение на группы пробелом, тогда на сервере логика выбора будет поле like %1группа% and поле like %2группа% и так далее. как правило редко приходится вбивать более 3-х групп для выборки аз 10 лямов записей - из практики применения. замыкания, конечно хорошо надо знать, но и применять надо по месту.
deamondz
24.06.2023 13:18+1а зачем это всё, если есть https://react.dev/reference/react/useDeferredValue?
vagon333
24.06.2023 13:18Если использовать не только в React, то вполне полезная функция.
Кстати, функция легко гуглится "function debounce(f, ms)", выдает компактный код и внятное объяснение.
Использую для сохранения последней выбранной записи, когда пользователь бегает по гриду.
Если сохранять каждый раз при переходе на запись - перегруз запросов. А если с задержкой в 2сек поесле последнего перехода - самое то.
nin-jin
24.06.2023 13:18-1А вот так дебонс выглядит в $mol:
@act someAction() { sleep( 1000 ) // do some work }
Нет, не продаю, просто показываю.
shasoftX
24.06.2023 13:18Для лучшего понимания тут бы наверное стоило или в таком виде задавать
@act 1000 someAction() { // do some work }
или вот в таком
@act someAction() { debonce( 1000 ) // do some work }
Иначе, ИМХО, стороннему человеку этот код реально кажется непонятным
nin-jin
24.06.2023 13:18Любая достаточно развитая технология неотличима от магии. Тут про это рассказывается: https://mol.hyoo.ru/#!section=docs/=97b2fn_qb9le1
dyadyaSerezha
timeoutId - не та переменная.
karmacan Автор
спасибо, поправил!