Одной из самых больших ошибок, которую я часто вижу при поиске оптимизации существующего кода, является отсутствие функции debounce
. Если твое веб-приложение использует JavaScript для выполнения задач, функция debounce
необходима для гарантии того, что определённая задача не запускается слишком часто, и тем самым не снижает производительность браузера.
Для тех, кто не знает, что делает функция debounce
она ограничивает скорость, с которой функция может срабатывать.
Это предотвращает необходимость обработки каждого пользовательского кода каждым событием, а также значительно уменьшает количество вызовов API, отправляемых на сервер.
Например: у нас есть listener изменения размера window, который выполняет некоторые измерения размеров элемента и (возможно) перемещает несколько элементов. Это не тяжелая задача, но его многократный запуск, после многочисленных изменений, действительно замедлит работу нашего сайта.
Функция debounce
может изменить правила игры, если речь идёт о производительности, вызванной событиями scroll
, resize
и key*
.
Вот функция debounce JavaScript (взятая из Underscore.js):
// Возвращает функцию, которая, пока она продолжает вызываться,
// не будет запускаться.
// Она будет вызвана один раз через N миллисекунд после последнего вызова.
// Если передано аргумент `immediate` (true), то она запустится сразу же при
// первом запуске функции.
function debounce(func, wait, immediate) {
let timeout;
return function executedFunction() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
Debounce - функция высшего порядка, которая возвращает другую функцию (для ясности, здесь она называется executedFunction
). Это делается для формирования замыкания (closure) вокруг параметров func
, wait
и immediate
и переменной timeout
, чтобы их значения сохранялись.
-
func
: функция, которую ты хочешь выполнить после определенного промежутка времени. -
wait
: отрезок времени, который функцияdebounce
будет ожидать после последнего полученного действия, прежде чем выполнятьfunc
. -
immediate
:true / false
определяет, должна ли функция вызываться вначале. Еслиtrue
- это означает, что мы вызываем функцию один раз сразу, а затем ждем, пока период ожиданияwait
не истечет после вызова. По истечении времени следующее событие вызовет функцию и перезапуститdebounce
. Еслиfalse
- ждём, пока не истечет период ожиданияwait
, а затем вызываем функцию. -
timeout
: значение, используемое для обозначения текущегоdebounce
.
Инициируем debounce
:
const returnedFunction = debounce(function() {
// Что то делаем
console.log('debounce');
}, 250);
window.addEventListener('resize', returnedFunction);
Так как функция debounce
возвращает функцию, функция executedFunction
из первого примера и функция returnedFunction
из второго представляют собой одну и ту же функцию. Каждый раз, когда window
меняется (resize), то будет выполнятся executedFunction / returnedFunction
.
Давай рассмотрим, что происходит внутри функции. Сначала сохраняем контекст this
и содержимое arguments
, передаваемых в executedFunction
. В JavaScript можем вызывать функцию с произвольным числом параметров, даже если их нет в определении функции, arguments
всё равно их захватит.
const context = this;
const args = arguments;
Далее, мы объявляем функцию обратного вызова later
. Она является функцией, которая выполняется после окончания debounce
. Это то, что будет вызвано после истечения setTimeout
.
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
timeout = setTimeout(later, wait);
Также, мы объявляем callNow
, который определяет, хотим ли мы вызвать функцию в начале. Если да, то вызов выполняется, при условии, что не запущен debounce
таймер, то есть timeout !== true
.
Событие callNow === true
приведет к немедленному выполнению функции func
, а затем предотвратит любые последующие вызовы, если не истек debounce
таймер.
const callNow = immediate && !timeout;
if (callNow) func.apply(context, args);
Через clearTimeout
убираем таймер, который препятствовал выполнению обратного вызова и, таким образом, перезапускаем debounce
. Затем (повторно) объявляем timeout
, который начинает период ожидания debounce
. Если полное время ожидания истекает до другого события, мы выполняем later
функцию обратного вызова. Устанавливаем timeout
на null
- означает что debounce
закончилась. Затем он проверяет, хотим ли мы вызвать debounce
функцию под конец. Если !immediate
, то выполняется func.apply(context, args)
. Apply
выполняет функцию с заданным значением this
и массивом arguments
.
Вот закомментированная версия функции:
function debounce(func, wait, immediate) {
let timeout;
// Эта функция выполняется, когда событие DOM вызвано.
return function executedFunction() {
// Сохраняем контекст this и любые параметры,
// переданные в executedFunction.
const context = this;
const args = arguments;
// Функция, вызываемая по истечению времени debounce.
const later = function() {
// Нулевой timeout, чтобы указать, что debounce закончилась.
timeout = null;
// Вызываем функцию, если immediate !== true,
// то есть, мы вызываем функцию в конце, после wait времени.
if (!immediate) func.apply(context, args);
};
// Определяем, следует ли нам вызывать функцию в начале.
const callNow = immediate && !timeout;
// clearTimeout сбрасывает ожидание при каждом выполнении функции.
// Это шаг, который предотвращает выполнение функции.
clearTimeout(timeout);
// Перезапускаем период ожидания debounce.
// setTimeout возвращает истинное значение / truthy value
// (оно отличается в web и node)
timeout = setTimeout(later, wait);
// Вызываем функцию в начале, если immediate === true
if (callNow) func.apply(context, args);
};
};
Распространенными сценариями для debounce
функции являются события resize
, scroll
и keyup/keydown
. Кроме того, мы должны рассмотреть возможность использовать debounce
при любом взаимодействии, которое вызывает чрезмерные вычисления или вызовы API
.
Рекомендую тебе взглянуть на Underscore.js и Lodash и их многочисленные вспомогательные функции.
Источники. JavaScript Debounce Function, Debounce in JavaScript