Может кто-нибудь объяснить функцию «debounce» в Javascript


151

Меня интересует функция «debouncing» в javascript, написанная здесь: http://davidwalsh.name/javascript-debounce-function

К сожалению, код недостаточно четко объяснен для меня, чтобы понять. Может кто-нибудь помочь мне разобраться, как это работает (я оставил свои комментарии ниже). Короче я просто очень не понимаю как это работает

   // Returns a function, that, as long as it continues to be invoked, will not
   // be triggered. The function will be called after it stops being called for
   // N milliseconds.


function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

РЕДАКТИРОВАТЬ: скопированный фрагмент кода ранее был callNowв неправильном месте.


1
Если вы звоните clearTimeoutс чем-то, что не является действительным идентификатором таймера, он ничего не делает.
Ry-

@false, это допустимое стандартное поведение?
Pacerier

3
@Pacerier Да, это в спецификации : «Если handle не идентифицирует запись в списке активных таймеров WindowTimersобъекта, для которого был вызван метод, метод ничего не делает».
Маттиас Буэленс

Ответы:


134

Код в вопросе был немного изменен по сравнению с кодом в ссылке. В ссылке есть проверка (immediate && !timeout)ДО создания нового тайм-аута. После этого немедленный режим никогда не срабатывает. Я обновил свой ответ, чтобы аннотировать рабочую версию по ссылке.

function debounce(func, wait, immediate) {
  // 'private' variable for instance
  // The returned function will be able to reference this due to closure.
  // Each call to the returned function will share this common timer.
  var timeout;

  // Calling debounce returns a new anonymous function
  return function() {
    // reference the context and args for the setTimeout function
    var context = this,
      args = arguments;

    // Should the function be called now? If immediate is true
    //   and not already in a timeout then the answer is: Yes
    var callNow = immediate && !timeout;

    // This is the basic debounce behaviour where you can call this 
    //   function several times, but it will only execute once 
    //   [before or after imposing a delay]. 
    //   Each time the returned function is called, the timer starts over.
    clearTimeout(timeout);

    // Set the new timeout
    timeout = setTimeout(function() {

      // Inside the timeout function, clear the timeout variable
      // which will let the next execution run when in 'immediate' mode
      timeout = null;

      // Check if the function already ran with the immediate flag
      if (!immediate) {
        // Call the original function with apply
        // apply lets you define the 'this' object as well as the arguments 
        //    (both captured before setTimeout)
        func.apply(context, args);
      }
    }, wait);

    // Immediate mode and no wait timer? Execute the function..
    if (callNow) func.apply(context, args);
  }
}

/////////////////////////////////
// DEMO:

function onMouseMove(e){
  console.clear();
  console.log(e.x, e.y);
}

// Define the debounced function
var debouncedMouseMove = debounce(onMouseMove, 50);

// Call the debounced function on every mouse move
window.addEventListener('mousemove', debouncedMouseMove);


1
для immediate && timeoutпроверки. Не всегда будет timeout(потому что timeoutназывается раньше). Кроме того, что хорошего в том, clearTimeout(timeout)что когда он объявляется (делает его неопределенным) и очищается раньше
Startec

immediate && !timeoutПроверка когда дребезг сконфигурирован с immediateфлагом. Это выполнит функцию немедленно, но установит waitтайм-аут раньше, если может быть выполнено снова. Таким образом, !timeoutчасть в основном говорит: «Извините, Буб, это уже было выполнено в определенном окне» ... помните, что функция setTimeout очистит его, позволив выполнить следующий вызов.
Малк

1
Почему тайм-аут должен быть установлен равным нулю внутри setTimeoutфункции? Кроме того, я попробовал этот код, для меня, передача trueдля немедленного просто предотвращает вызов функции вообще (а не вызывается после задержки). Это случилось для вас?
Startec

У меня есть похожий вопрос о немедленном? почему он должен иметь непосредственный параметр. Установка ожидания на 0 должна иметь тот же эффект, верно? И, как заметил @Startec, это поведение довольно странное.
Зеролю

2
Если вы просто вызываете функцию, вы не можете навязать таймер ожидания, прежде чем эту функцию можно будет вызвать снова. Подумайте об игре, в которой пользователь нажимает клавишу зажигания. Вы хотите, чтобы этот огонь срабатывал немедленно, но не запускался снова в течение еще нескольких миллисекунд, независимо от того, насколько быстро пользователь нажимает кнопку.
Малк

57

Здесь важно отметить, что debounceсоздается функция, которая «закрывается» над timeoutпеременной. В timeoutпеременной остается доступным при каждом вызове функции производится даже после того, как debounceсам вернулся, и может меняться в течение различных вызовов.

Общая идея debounceзаключается в следующем:

  1. Начните без перерыва.
  2. Если вызванная функция вызвана, очистите и сбросьте тайм-аут.
  3. Если истекло время ожидания, вызовите оригинальную функцию.

Первый пункт справедлив var timeout;, это действительно справедливо undefined. К счастью, clearTimeoutэто довольно слабо в отношении ввода: передача undefinedидентификатора таймера заставляет его просто ничего не делать, он не выдает ошибку или что-то в этом роде.

Второй пункт сделан произведенной функцией. Сначала он хранит некоторую информацию о вызове ( thisконтекст и arguments) в переменных, чтобы затем использовать их для отклоненного вызова. Затем он очищает тайм-аут (если был один набор), а затем создает новый, чтобы заменить его, используя setTimeout. Обратите внимание, что это перезаписывает значение, timeoutи это значение сохраняется в течение нескольких вызовов функций! Это позволяет debounce фактически работать: если функция вызывается несколько раз, timeoutона перезаписывается несколько раз новым таймером. Если бы это было не так, то несколько вызовов могут вызвать несколько таймеров , чтобы быть запущен , которые все остаются активными - звонки будут просто задерживаются, но не подавление дребезга контактов.

Третий пункт делается в обратном вызове тайм-аута. Он сбрасывает timeoutпеременную и выполняет фактический вызов функции, используя сохраненную информацию о вызове.

Предполагается, что immediateфлаг контролирует, должна ли функция вызываться до или после таймера. Если это false, исходная функция не не вызывается , пока после того, как таймер хит. Если это так true, исходная функция вызывается первой и больше не будет вызываться, пока не будет нажата таймер.

Тем не менее, я считаю, что if (immediate && !timeout)проверка неверна: timeoutтолько что был установлен идентификатор таймера, возвращаемый setTimeoutтак !timeout, всегда falseв этой точке, и, следовательно, функция никогда не может быть вызвана. Текущая версия underscore.js, кажется, имеет немного другую проверку, где она оценивает immediate && !timeout перед вызовом setTimeout. (Алгоритм также немного отличается, например, он не использует clearTimeout.) Вот почему вы всегда должны стараться использовать последнюю версию ваших библиотек. :-)


«Обратите внимание, что это перезаписывает значение тайм-аута, и это значение сохраняется в течение нескольких вызовов функций». Разве тайм-аут не является локальным для каждого вызова debounce? Это объявлено с вар. Как это перезаписывается каждый раз? Кроме того, зачем проверять !timeoutв конце? Почему он не существует всегда (потому что он установленsetTimeout(function() etc.)
Startec

2
@Startec Это локально для каждого вызова debounce, да, но оно распределяется между вызовами возвращаемой функции (той функции, которую вы собираетесь использовать). Так , например, в g = debounce(f, 100)значении timeoutсохраняется в течение нескольких звонков g. !timeoutПроверка в конце концов , это ошибка , которую я верю, и это не в текущем underscore.js коды.
Маттиас Буэленс

Почему тайм-аут необходимо очистить в начале функции возврата (сразу после ее объявления)? Кроме того, тогда оно устанавливается равным нулю внутри функции setTimeout. Разве это не избыточно? (Сначала он очищается, а затем устанавливается на null. В моих тестах с приведенным выше кодом установка немедленного значения в true делает функцию вообще не вызываемой, как вы упомянули. Любое решение без подчеркивания?
Startec

34

Отклоненные функции не выполняются при вызове, они ждут паузу вызовов в течение настраиваемой продолжительности перед выполнением; каждый новый вызов перезапускает таймер.

Дросселированные функции выполняются, а затем ждут настраиваемую длительность, прежде чем снова смогут запускаться.

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

Throttle отлично подходит для конечных точек в реальном времени, которые вы хотите разрешить пользователю вызывать только один раз за определенный период времени.

Проверьте Underscore.js для их реализации тоже.


25

Я написал пост под названием « Демистификация Debounce в JavaScript», где я объясняю, как именно работает функция debounce, и включаю демонстрацию.

Я тоже не до конца понимал, как работает функция debounce, когда я впервые столкнулся с ней. Хотя они относительно небольшие по размеру, они на самом деле используют довольно продвинутые концепции JavaScript! Хорошее управление прицелом, замыканиями и setTimeoutметодом поможет.

С учетом вышесказанного ниже приведена базовая функция debounce, которая была объяснена и продемонстрирована в моем посте, указанном выше.

Готовый продукт

// Create JD Object
// ----------------
var JD = {};

// Debounce Method
// ---------------
JD.debounce = function(func, wait, immediate) {
    var timeout;
    return function() {
        var context = this,
            args = arguments;
        var later = function() {
            timeout = null;
            if ( !immediate ) {
                func.apply(context, args);
            }
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait || 200);
        if ( callNow ) { 
            func.apply(context, args);
        }
    };
};

Объяснение

// Create JD Object
// ----------------
/*
    It's a good idea to attach helper methods like `debounce` to your own 
    custom object. That way, you don't pollute the global space by 
    attaching methods to the `window` object and potentially run in to
    conflicts.
*/
var JD = {};

// Debounce Method
// ---------------
/*
    Return a function, that, as long as it continues to be invoked, will
    not be triggered. The function will be called after it stops being 
    called for `wait` milliseconds. If `immediate` is passed, trigger the 
    function on the leading edge, instead of the trailing.
*/
JD.debounce = function(func, wait, immediate) {
    /*
        Declare a variable named `timeout` variable that we will later use 
        to store the *timeout ID returned by the `setTimeout` function.

        *When setTimeout is called, it retuns a numeric ID. This unique ID
        can be used in conjunction with JavaScript's `clearTimeout` method 
        to prevent the code passed in the first argument of the `setTimout`
        function from being called. Note, this prevention will only occur
        if `clearTimeout` is called before the specified number of 
        milliseconds passed in the second argument of setTimeout have been
        met.
    */
    var timeout;

    /*
        Return an anomymous function that has access to the `func`
        argument of our `debounce` method through the process of closure.
    */
    return function() {

        /*
            1) Assign `this` to a variable named `context` so that the 
               `func` argument passed to our `debounce` method can be 
               called in the proper context.

            2) Assign all *arugments passed in the `func` argument of our
               `debounce` method to a variable named `args`.

            *JavaScript natively makes all arguments passed to a function
            accessible inside of the function in an array-like variable 
            named `arguments`. Assinging `arguments` to `args` combines 
            all arguments passed in the `func` argument of our `debounce` 
            method in a single variable.
        */
        var context = this,   /* 1 */
            args = arguments; /* 2 */

        /*
            Assign an anonymous function to a variable named `later`.
            This function will be passed in the first argument of the
            `setTimeout` function below.
        */
        var later = function() {

            /*      
                When the `later` function is called, remove the numeric ID 
                that was assigned to it by the `setTimeout` function.

                Note, by the time the `later` function is called, the
                `setTimeout` function will have returned a numeric ID to 
                the `timeout` variable. That numeric ID is removed by 
                assiging `null` to `timeout`.
            */
            timeout = null;

            /*
                If the boolean value passed in the `immediate` argument 
                of our `debouce` method is falsy, then invoke the 
                function passed in the `func` argument of our `debouce`
                method using JavaScript's *`apply` method.

                *The `apply` method allows you to call a function in an
                explicit context. The first argument defines what `this`
                should be. The second argument is passed as an array 
                containing all the arguments that should be passed to 
                `func` when it is called. Previously, we assigned `this` 
                to the `context` variable, and we assigned all arguments 
                passed in `func` to the `args` variable.
            */
            if ( !immediate ) {
                func.apply(context, args);
            }
        };

        /*
            If the value passed in the `immediate` argument of our 
            `debounce` method is truthy and the value assigned to `timeout`
            is falsy, then assign `true` to the `callNow` variable.
            Otherwise, assign `false` to the `callNow` variable.
        */
        var callNow = immediate && !timeout;

        /*
            As long as the event that our `debounce` method is bound to is 
            still firing within the `wait` period, remove the numerical ID  
            (returned to the `timeout` vaiable by `setTimeout`) from 
            JavaScript's execution queue. This prevents the function passed 
            in the `setTimeout` function from being invoked.

            Remember, the `debounce` method is intended for use on events
            that rapidly fire, ie: a window resize or scroll. The *first* 
            time the event fires, the `timeout` variable has been declared, 
            but no value has been assigned to it - it is `undefined`. 
            Therefore, nothing is removed from JavaScript's execution queue 
            because nothing has been placed in the queue - there is nothing 
            to clear.

            Below, the `timeout` variable is assigned the numerical ID 
            returned by the `setTimeout` function. So long as *subsequent* 
            events are fired before the `wait` is met, `timeout` will be 
            cleared, resulting in the function passed in the `setTimeout` 
            function being removed from the execution queue. As soon as the 
            `wait` is met, the function passed in the `setTimeout` function 
            will execute.
        */
        clearTimeout(timeout);

        /*
            Assign a `setTimout` function to the `timeout` variable we 
            previously declared. Pass the function assigned to the `later` 
            variable to the `setTimeout` function, along with the numerical 
            value assigned to the `wait` argument in our `debounce` method. 
            If no value is passed to the `wait` argument in our `debounce` 
            method, pass a value of 200 milliseconds to the `setTimeout` 
            function.  
        */
        timeout = setTimeout(later, wait || 200);

        /*
            Typically, you want the function passed in the `func` argument
            of our `debounce` method to execute once *after* the `wait` 
            period has been met for the event that our `debounce` method is 
            bound to (the trailing side). However, if you want the function 
            to execute once *before* the event has finished (on the leading 
            side), you can pass `true` in the `immediate` argument of our 
            `debounce` method.

            If `true` is passed in the `immediate` argument of our 
            `debounce` method, the value assigned to the `callNow` variable 
            declared above will be `true` only after the *first* time the 
            event that our `debounce` method is bound to has fired.

            After the first time the event is fired, the `timeout` variable
            will contain a falsey value. Therfore, the result of the 
            expression that gets assigned to the `callNow` variable is 
            `true` and the function passed in the `func` argument of our
            `debounce` method is exected in the line of code below.

            Every subsequent time the event that our `debounce` method is 
            bound to fires within the `wait` period, the `timeout` variable 
            holds the numerical ID returned from the `setTimout` function 
            assigned to it when the previous event was fired, and the 
            `debounce` method was executed.

            This means that for all subsequent events within the `wait`
            period, the `timeout` variable holds a truthy value, and the
            result of the expression that gets assigned to the `callNow`
            variable is `false`. Therefore, the function passed in the 
            `func` argument of our `debounce` method will not be executed.  

            Lastly, when the `wait` period is met and the `later` function
            that is passed in the `setTimeout` function executes, the 
            result is that it just assigns `null` to the `timeout` 
            variable. The `func` argument passed in our `debounce` method 
            will not be executed because the `if` condition inside the 
            `later` function fails. 
        */
        if ( callNow ) { 
            func.apply(context, args);
        }
    };
};

1

Что вы хотите сделать, это следующее: если вы пытаетесь вызвать функцию сразу за другой, первая должна быть отменена, а новая должна подождать заданное время ожидания и затем выполнить. Таким образом, в действительности вам нужен способ отменить таймаут первой функции? Но как? Вы можете вызвать функцию и передать возвращаемый таймаут-идентификатор, а затем передать этот идентификатор в любые новые функции. Но решение, приведенное выше, более элегантно.

Что он делает, так это делает timeoutпеременную доступной в области возвращаемой функции. Поэтому, когда происходит событие «resize», оно больше не вызывается debounce(), следовательно, timeoutсодержимое не изменяется (!) И все еще доступно для «следующего вызова функции».

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

var events = ['resize', 'resize', 'resize'];
var timeout = null;
for (var i = 0; i < events.length; i++){
    if (immediate && !timeout) func.apply(this, arguments);
    clearTimeout(timeout); // does not do anything if timeout is null.
    timeout = setTimeout(function(){
        timeout = null;
        if (!immediate) func.apply(this, arguments);
    }
}

Вы видите, что timeoutдоступно для следующей итерации? И нет никаких причин, по - моему переименован thisв contentи argumentsк args.


«Переименование» абсолютно необходимо. Значение thisи argumentsизменения внутри функции обратного вызова setTimeout (). Вы должны сохранить копию в другом месте или эта информация будет потеряна.
CubicleSoft

1

Это вариант, который всегда запускает дебатированную функцию при первом вызове с более описательно названными переменными:

function debounce(fn, wait = 1000) {
  let debounced = false;
  let resetDebouncedTimeout = null;
  return function(...args) {
    if (!debounced) {
      debounced = true;
      fn(...args);
      resetDebouncedTimeout = setTimeout(() => {
        debounced = false;
      }, wait);
    } else {
      clearTimeout(resetDebouncedTimeout);
      resetDebouncedTimeout = setTimeout(() => {
        debounced = false;
        fn(...args);
      }, wait);
    }
  }
};

1

Простой метод Debounce в JavaScript

<!-- Basic HTML -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Debounce Method</title>
</head>
<body>
  <button type="button" id="debounce">Debounce Method</button><br />
  <span id="message"></span>
</body>
</html>

  // JS File
  var debouncebtn = document.getElementById('debounce');
    function debounce(func, delay){
      var debounceTimer;
      return function () {
        var context = this, args = arguments;
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(function() {
          func.apply(context, args)
        }, delay);
      }
    }

// Driver Code
debouncebtn.addEventListener('click', debounce(function() {
    document.getElementById('message').innerHTML += '<br/> Button only triggeres is every 3 secounds how much every you fire an event';
  console.log('Button only triggeres in every 3 secounds how much every you fire an event');
},3000))

Пример выполнения JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Простая функция debounce: -

HTML: -

<button id='myid'>Click me</button>

Javascript: -

    function debounce(fn, delay) {
      let timeoutID;
      return function(...args){
          if(timeoutID) clearTimeout(timeoutID);
          timeoutID = setTimeout(()=>{
            fn(...args)
          }, delay);
      }
   }

document.getElementById('myid').addEventListener('click', debounce(() => {
  console.log('clicked');
},2000));
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.