Qualcuno può spiegare la funzione "debounce" in Javascript


151

Sono interessato alla funzione "debouncing" in javascript, scritta qui: http://davidwalsh.name/javascript-debounce-function

Sfortunatamente il codice non è spiegato in modo abbastanza chiaro per me da capire. Qualcuno può aiutarmi a capire come funziona (ho lasciato i miei commenti qui sotto). In breve, proprio non capisco come funzioni

   // 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: lo snippet di codice copiato in precedenza si trovava callNownella posizione sbagliata.


1
Se chiami clearTimeoutcon qualcosa che non è un ID timer valido, non fa nulla.
Ry-

@false, è un comportamento standard valido?
Pacerier,

3
@Pacerier Sì, è nella specifica : "Se handle non identifica una voce nell'elenco di timer attivi WindowTimersdell'oggetto su cui è stato invocato il metodo, il metodo non fa nulla".
Mattias Buelens,

Risposte:


134

Il codice nella domanda è stato leggermente modificato rispetto al codice nel collegamento. Nel collegamento è presente un controllo (immediate && !timeout)PRIMA di creare un nuovo tempo. Avendolo dopo, la modalità immediata non si attiva mai. Ho aggiornato la mia risposta per annotare la versione funzionante dal link.

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
per il immediate && timeoutcontrollo. Non ci sarà sempre un timeout(perché timeoutsi chiama prima). Inoltre, cosa fa il clearTimeout(timeout)bene, quando viene dichiarato (rendendolo indefinito) e cancellato, prima
Startec

Il immediate && !timeoutcontrollo è per quando il debounce è configurato con il immediateflag. Questo eseguirà immediatamente la funzione ma imporrà un waittimeout prima che possa essere eseguito di nuovo. Quindi la !timeoutparte in pratica dice "scusa bub, questo è già stato eseguito all'interno della finestra definita" ... ricorda che la funzione setTimeout lo cancellerà, consentendo l'esecuzione della chiamata successiva.
Malk

1
Perché il timeout deve essere impostato su null all'interno della setTimeoutfunzione? Inoltre, ho provato questo codice, per me, passare trueimmediatamente per impedire che la funzione venga chiamata (piuttosto che essere chiamata dopo un ritardo). Questo succede per te?
Startec,

Ho una domanda simile sull'immediato? perché deve avere il param immediato? L'impostazione di wait su 0 dovrebbe avere lo stesso effetto, giusto? E come ha detto @Startec, questo comportamento è piuttosto strano.
Zeroliu,

2
Se si chiama semplicemente la funzione, non è possibile imporre un timer di attesa prima che tale funzione possa essere richiamata di nuovo. Pensa a un gioco in cui l'utente schiaccia la chiave di fuoco. Volete che il fuoco si attivi immediatamente, ma non sparate di nuovo per altri X millisecondi, non importa quanto velocemente l'utente schiaccia il pulsante.
Malk,

57

La cosa importante da notare qui è che debounceproduce una funzione che è "chiusa" sulla timeoutvariabile. La timeoutvariabile rimane accessibile durante ogni chiamata della funzione prodotta anche dopo che debounceè stata restituita, e può cambiare su chiamate diverse.

L'idea generale per debounceè la seguente:

  1. Inizia senza timeout.
  2. Se viene chiamata la funzione prodotta, cancellare e ripristinare il timeout.
  3. Se si verifica il timeout, chiamare la funzione originale.

Il primo punto è giusto var timeout;, è davvero giusto undefined. Fortunatamente, clearTimeoutè piuttosto lassista riguardo al suo input: passare un undefinedidentificatore del timer gli fa semplicemente non fare nulla, non genera un errore o qualcosa del genere.

Il secondo punto è svolto dalla funzione prodotta. Prima memorizza alcune informazioni sulla chiamata (il thiscontesto e il arguments) in variabili in modo da poterle successivamente utilizzare per la chiamata rimandata. Quindi cancella il timeout (se ce n'era uno impostato) e quindi ne crea uno nuovo per sostituirlo utilizzando setTimeout. Nota che questo sovrascrive il valore di timeoute questo valore persiste su più chiamate di funzione! Ciò consente al debounce di funzionare effettivamente: se la funzione viene chiamata più volte, timeoutviene sovrascritta più volte con un nuovo timer. Se così non fosse, più chiamate causerebbero l'avvio di più timer che rimarrebbero tutti attivi - le chiamate verrebbero semplicemente ritardate, ma non rimandate.

Il terzo punto viene eseguito nel callback del timeout. Disattiva la timeoutvariabile ed esegue la chiamata di funzione effettiva utilizzando le informazioni sulla chiamata memorizzata.

Il immediateflag dovrebbe controllare se la funzione deve essere chiamata prima o dopo il timer. In tal caso false, la funzione originale non viene chiamata fino a quando non viene premuto il timer. In tal caso true, la funzione originale viene prima chiamata e non verrà più chiamata fino a quando non viene premuto il timer.

Tuttavia, credo che il if (immediate && !timeout)controllo sia errato: timeoutè appena stato impostato l'identificatore del timer restituito, setTimeoutquindi !timeoutè sempre falsea quel punto e quindi la funzione non può mai essere chiamata. La versione corrente di underscore.js sembra avere un controllo leggermente diverso, dove valuta immediate && !timeout prima di chiamare setTimeout. (L'algoritmo è anche un po 'diverso, ad esempio non lo utilizza clearTimeout.) Ecco perché dovresti sempre provare a utilizzare l'ultima versione delle tue librerie. :-)


"Si noti che questo sovrascrive il valore del timeout e questo valore persiste per più chiamate di funzione" Il timeout non è locale per ciascuna chiamata di rinvio? È dichiarato con var. Come viene sovrascritto ogni volta? Inoltre, perché controllare !timeoutalla fine? Perché non esiste sempre (perché è impostato susetTimeout(function() etc.)
Startec

2
@Startec È locale per ogni chiamata di debounce, sì, ma è condiviso tra le chiamate alla funzione restituita (che è la funzione che si intende utilizzare). Ad esempio, in g = debounce(f, 100), il valore di timeoutpersiste su più chiamate a g. Il !timeoutcontrollo alla fine è un errore, credo, e non è nel codice underscore.js corrente.
Mattias Buelens,

Perché il timeout deve essere cancellato presto nella funzione di ritorno (subito dopo che è stato dichiarato)? Inoltre, viene quindi impostato su null all'interno della funzione setTimeout. Non è ridondante? (Prima viene cancellato, quindi è impostato su null. Nei miei test con il codice sopra, l'impostazione immediata su true rende la funzione non chiamata affatto, come hai detto. Qualche soluzione senza carattere di sottolineatura?
Startec

34

Le funzioni debounce non vengono eseguite quando invocate, prima di essere eseguite attendono una pausa di chiamate per una durata configurabile; ogni nuova chiamata riavvia il timer.

Le funzioni limitate vengono eseguite e quindi attendere una durata configurabile prima di poter essere nuovamente abilitate al fuoco.

Debounce è ottimo per gli eventi di pressione dei tasti; quando l'utente inizia a digitare e poi mette in pausa, invia tutte le pressioni dei tasti come un singolo evento, riducendo così le chiamate di gestione.

La limitazione è ottima per gli endpoint in tempo reale che si desidera consentire all'utente di invocare una sola volta per un determinato periodo di tempo.

Dai un'occhiata a Underscore.js anche per le loro implementazioni.


25

Ho scritto un post intitolato Demistifying Debounce in JavaScript in cui spiego esattamente come funziona una funzione di debounce e includo una demo.

Anch'io non ho capito appieno come funzionava una funzione di rimbalzo quando ne ho incontrata una. Sebbene di dimensioni relativamente ridotte, in realtà utilizzano alcuni concetti JavaScript piuttosto avanzati! Avere una buona presa su portata, chiusure e setTimeoutmetodo aiuterà.

Detto questo, di seguito è la funzione di debounce di base spiegata e dimostrata nel mio post di cui sopra.

Il prodotto finito

// 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);
        }
    };
};

La spiegazione

// 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

Quello che vuoi fare è il seguente: Se provi a chiamare una funzione subito dopo l'altra, la prima dovrebbe essere annullata e la nuova dovrebbe attendere un determinato timeout e quindi eseguire. Quindi in effetti hai bisogno di un modo per annullare il timeout della prima funzione? Ma come? si potrebbe chiamare la funzione e passare l'ID timeout di ritorno e quindi passare tale ID in tutte le nuove funzioni. Ma la soluzione sopra è molto più elegante.

Ciò che fa è effettivamente rendere timeoutdisponibile la variabile nell'ambito della funzione restituita. Pertanto, quando viene generato un evento "ridimensionamento", questo non viene richiamato di debounce()nuovo, quindi il timeoutcontenuto non viene modificato (!) E rimane disponibile per la "chiamata di funzione successiva".

La cosa fondamentale qui è fondamentalmente che chiamiamo la funzione interna ogni volta che abbiamo un evento di ridimensionamento. Forse è più chiaro se immaginiamo che tutti gli eventi di ridimensionamento siano in un array:

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);
    }
}

Vedi che timeoutè disponibile per la prossima iterazione? E non c'è motivo, secondo me, di rinominare thisin contente argumentsto args.


"Rinomina" è assolutamente necessario. Il significato thise la argumentsmodifica della funzione di callback setTimeout (). Devi conservarne una copia altrove o le informazioni andranno perse.
CubicleSoft

1

Questa è una variante che attiva sempre la funzione rimbalzata la prima volta che viene chiamata, con variabili con nomi più descrittivi:

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

Metodo Debounce semplice in 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))

Esempio di runtime JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Funzione di debounce semplice: -

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));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.