Главная Категории Контакты Поиск

Debounce в JavaScript

Объяснение функции debounce и рекомендации о том, как её использовать в своем коде JavaScript для повышения производительности.

JavaScript ·14.01.2020·читать 4 мин 🤓·Автор: Alex Myzgin

Одной из самых больших ошибок, которую я часто вижу при поиске оптимизации существующего кода, является отсутствие функции 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

Website, name & logo
Copyright © 2019. Alex Myzgin