Чи може хтось пояснити функцію "debounce" у Javascript


151

Мене цікавить функція "деблокування" в 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);
    };
};

EDIT: Скопійований фрагмент коду раніше знаходився callNowв неправильному місці.


1
Якщо ви телефонуєте clearTimeoutз чимось, що не є дійсним ідентифікатором таймера, він нічого не робить.
Ри-

@false, це дійсна стандартна поведінка?
Pacerier

3
@Pacerier Так, це в специфікації : "Якщо обробка не ідентифікує запис у списку активних таймерів 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, така поведінка досить дивна.
zeroliu

2
Якщо ви просто зателефонували до функції, ви не можете накласти таймер очікування, перш ніж цю функцію можна буде викликати знову. Подумайте про гру, де користувач розтирає ключ вогню. Ви хочете, щоб цей вогонь спрацьовував негайно, але не повторювався ще протягом X мілісекунд незалежно від того, наскільки швидко користувач натискає кнопку.
Бесіда

57

Тут важливо зауважити, що debounceвиробляється функція, яка "закрита" timeoutзмінною. Ця timeoutзмінна залишається доступною під час кожного дзвінка виробленої функції навіть після того, debounceяк вона сама повертається, і може змінюватися в різних дзвінках.

Загальна ідея debounceполягає в наступному:

  1. Почніть без закінчення часу.
  2. Якщо викликана функція викликається, очистіть і скиньте час очікування.
  3. Якщо час очікується, зателефонуйте до оригінальної функції.

Перший пункт справедливий var timeout;, він справді справедливий undefined. На щастя, clearTimeoutдосить в'ялим щодо свого введення: передача undefinedідентифікатора таймера змушує його просто нічого не робити, він не кидає помилку чи щось таке.

Другий момент робиться виробленою функцією. Спочатку зберігається деяка інформація про виклик ( thisконтекст і arguments) у змінних, щоб згодом їх використовувати для дебютованого виклику. Потім він очищає тайм-аут (якщо був один набір), а потім створює новий, щоб замінити його за допомогою setTimeout. Зауважте, що це перезаписує значення, timeoutі це значення зберігається протягом декількох викликів функцій! Це дозволяє дебюту реально працювати: якщо функція викликається кілька разів, timeoutперезаписується кілька разів новим таймером. Якби цього не було, багатократні дзвінки спричинили б запуск декількох таймерів, які всі залишаються активними - дзвінки просто затримуються, але не знімаються з нього.

Третій момент робиться в зворотному режимі очікування виклику. Він скасовує timeoutзмінну і робить фактичний виклик функції, використовуючи збережену інформацію про дзвінки.

immediateПрапор повинен контролювати , чи повинна функція бути викликана до того чи після того, як таймер. Якщо це false, вихідна функції не не викликається , поки після того, як таймер хіт. Якщо це true, оригінальна функція перший називається і буде називатися не більше , поки таймер не потрапив.

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


"Зверніть увагу, що це перезаписує значення таймауту, і це значення зберігається протягом декількох викликів функцій. Він оголошений з var. Як це переписується кожен раз? Крім того, навіщо перевіряти !timeoutв кінці? Чому його не існує завжди (адже він встановлений наsetTimeout(function() etc.)
Startec

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

Чому тайм-аут потрібно очистити на початку повернення функції (одразу після його оголошення)? Крім того, вона встановлюється в нульову форму всередині функції setTimeout. Хіба це не зайве? (Спочатку він очищається, потім він встановлюється null. У моїх тестах з вищевказаним кодом встановлення негайного значення true відповідає функції, яка взагалі не викликає, як ви згадали. Будь-яке рішення без підкреслення?
Startec

34

Дебютовані функції не виконуються при виклику, вони чекають паузи викликів протягом налаштованої тривалості перед виконанням; кожна нова виклик перезапускає таймер.

Заглушені функції виконують, а потім чекають налаштованої тривалості, перш ніж матимуть право на повторний запуск.

Debounce чудово підходить для подій натискання клавіш; коли користувач починає набирати текст, а потім призупиняє, ви подаєте всі натискання клавіш як одну подію, тим самим скорочуючи виклики обробки.

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

Ознайомтесь і з Underscore.js щодо їх реалізації.


24

Я написав допис під назвою Demistifying Debounce в JavaScript, де я точно пояснюю, як працює функція debounce, і включаю демонстрацію.

Я теж не повністю розумів, як функція дебютації працювала, коли я вперше зіткнулася з такою. Хоча відносно невеликі за розміром, вони насправді використовують деякі просунуті концепції JavaScript! Добре керуючись сферою застосування, закриттям та setTimeoutметодом допоможуть.

З урахуванням сказаного нижче, основна функція дебютування пояснюється та демонструється в моєму пості, на який згадувалося вище.

Готовий виріб

// 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змінну доступною в області повернутої функції. Отже, коли запускається подія "змінити розмір", вона не викликається debounce()знову, отже, timeoutвміст не змінюється (!) І все ще доступний для "наступного виклику функції".

Тут головне, що ми викликаємо внутрішню функцію кожного разу, коли ми маємо подію зміни розміру. Можливо, це зрозуміліше, якщо ми уявимо, що всі події розміру є у масиві:

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

Проста функція дебютування: -

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.