¿Alguien puede explicar la función "debounce" en Javascript

151

Estoy interesado en la función "antirrebote" en javascript, escrito aquí: http://davidwalsh.name/javascript-debounce-function

Lamentablemente, el código no se explica con suficiente claridad para que yo lo entienda. ¿Alguien puede ayudarme a entender cómo funciona? (Dejé mis comentarios a continuación). En resumen, realmente no entiendo cómo funciona esto

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

EDITAR: el fragmento de código copiado anteriormente se encontraba callNowen el lugar equivocado.

Startec
fuente
1
Si llama clearTimeoutcon algo que no es un ID de temporizador válido, no hace nada.
Ry-
@false, ¿es ese comportamiento estándar válido?
Pacerier
3
@Pacerier Sí, está en la especificación : "Si el identificador no identifica una entrada en la lista de temporizadores activos del WindowTimersobjeto en el que se invocó el método, el método no hace nada".
Mattias Buelens

Respuestas:

134

El código en la pregunta fue alterado ligeramente del código en el enlace. En el enlace, hay (immediate && !timeout)una marca ANTES de crear un nuevo tiempo de espera. Tenerlo después hace que el modo inmediato nunca se dispare. He actualizado mi respuesta para anotar la versión de trabajo desde el enlace.

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

Malk
fuente
1
para el immediate && timeoutcheque No siempre habrá un timeout(porque timeoutse llama antes). Además, qué bien hace clearTimeout(timeout), cuando se declara (lo que lo hace indefinido) y se borra, antes
Startec
La immediate && !timeoutverificación es para cuando debounce está configurado con la immediatebandera. Esto ejecutará la función inmediatamente pero impondrá un waittiempo de espera antes si puede ejecutarse nuevamente. Entonces, la !timeoutparte básicamente dice 'lo siento bub, esto ya se ejecutó dentro de la ventana definida' ... recuerda que la función setTimeout lo borrará, permitiendo que se ejecute la próxima llamada.
Malk
1
¿Por qué el tiempo de espera debe establecerse en nulo dentro de la setTimeoutfunción? Además, he probado este código, para mí, pasar truede inmediato solo evita que se llame a la función (en lugar de que se llame después de un retraso). ¿Esto te pasa a ti?
Startec
Tengo una pregunta similar acerca de inmediato? ¿Por qué necesita tener el parámetro inmediato? Establecer esperar a 0 debería tener el mismo efecto, ¿verdad? Y como mencionó @Startec, este comportamiento es bastante extraño.
zeroliu
2
Si solo llama a la función, no puede imponer un temporizador de espera antes de que se pueda volver a llamar a esa función. Piense en un juego donde el usuario aprieta la tecla de disparo. Desea que el disparo se active de inmediato, pero que no se dispare nuevamente durante otros X milisegundos, sin importar qué tan rápido el usuario apriete el botón.
Malk
57

Lo importante a tener en cuenta aquí es que debounceproduce una función que está "cerrada" sobre la timeoutvariable. La timeoutvariable permanece accesible durante cada llamada de la función producida, incluso después de que debouncehaya regresado, y puede cambiar entre diferentes llamadas.

La idea general para debouncees la siguiente:

  1. Comience sin tiempo de espera.
  2. Si se llama a la función producida, borre y restablezca el tiempo de espera.
  3. Si se alcanza el tiempo de espera, llame a la función original.

El primer punto es solo var timeout; , de hecho es justo undefined. Afortunadamente, clearTimeoutes bastante laxo sobre su entrada: pasar un undefinedidentificador de temporizador hace que simplemente no haga nada, no arroja un error o algo así.

El segundo punto lo realiza la función producida. Primero almacena cierta información sobre la llamada (el thiscontexto y el arguments) en variables para que luego pueda usarla para la llamada sin rebote. Luego borra el tiempo de espera (si había un conjunto) y luego crea uno nuevo para reemplazarlo usando setTimeout. Tenga en cuenta que esto sobrescribe el valor detimeout y este valor persiste en varias llamadas a funciones. Esto permite que el rebote funcione: si la función se llama varias veces, timeoutse sobrescribe varias veces con un nuevo temporizador. Si este no fuera el caso, varias llamadas provocarían que se iniciaran múltiples temporizadores, todos los cuales permanecerían activos; las llamadas simplemente se retrasarían, pero no se cancelarían.

El tercer punto se realiza en la devolución de llamada de tiempo de espera. Desarma la timeoutvariable y realiza la llamada a la función real utilizando la información de la llamada almacenada.

Se immediatesupone que la bandera controla si la función debe llamarse antes o después del temporizador. Si se trata false, la función original no se conoce hasta después de que el temporizador es golpeado. Si es así true, primero se llama a la función original y no se volverá a llamar hasta que se toque el temporizador.

Sin embargo, creo que la if (immediate && !timeout)verificación es incorrecta: timeoutacaba de establecerse en el identificador del temporizador devuelto por setTimeoutlo !timeoutque siempre está falseen ese punto y, por lo tanto, la función nunca se puede llamar. La versión actual de underscore.js parece tener una comprobación ligeramente diferente, donde se evalúa immediate && !timeout antes de llamarsetTimeout . (El algoritmo también es un poco diferente, por ejemplo, no se usa clearTimeout). Es por eso que siempre debe intentar usar la última versión de sus bibliotecas. :-)

Mattias Buelens
fuente
"Tenga en cuenta que esto sobrescribe el valor del tiempo de espera y este valor persiste durante varias llamadas de función" ¿No es el tiempo de espera local para cada llamada de rebote? Se declara con la var. ¿Cómo se sobrescribe cada vez? Además, ¿por qué buscar !timeoutal final? ¿Por qué no siempre existe (porque está configurado comosetTimeout(function() etc.)
Startec
2
@Startec Es local para cada llamada de debounce, sí, pero se comparte entre las llamadas a la función devuelta (que es la función que va a utilizar). Por ejemplo, en g = debounce(f, 100), el valor de timeoutpersiste en múltiples llamadas a g. El !timeoutcheque al final es un error, creo, y no está en el código actual de subrayado.js.
Mattias Buelens
¿Por qué es necesario borrar el tiempo de espera temprano en la función de retorno (justo después de que se declara)? Además, se establece en nulo dentro de la función setTimeout. ¿No es esto redundante? (Primero se borra, luego se establece en null. En mis pruebas con el código anterior, establecer inmediatamente en verdadero hace que la función no llame en absoluto, como usted mencionó. ¿Alguna solución sin guión bajo?
Startec
34

Las funciones rechazadas no se ejecutan cuando se invocan, esperan una pausa de invocaciones durante una duración configurable antes de ejecutarse; cada nueva invocación reinicia el temporizador.

Las funciones reguladas se ejecutan y luego esperan una duración configurable antes de ser elegibles para disparar nuevamente.

Debounce es ideal para eventos de pulsación de teclas; cuando el usuario comienza a escribir y luego hace una pausa, envía todas las pulsaciones de teclas como un solo evento, reduciendo así las invocaciones de manejo.

Throttle es ideal para puntos finales en tiempo real que solo desea permitir que el usuario invoque una vez por un período de tiempo establecido.

Echa un vistazo a Underscore.js para sus implementaciones también.

jurassix
fuente
24

Escribí una publicación titulada Demistifying Debounce en JavaScript donde explico exactamente cómo funciona una función antirrebote e incluyo una demostración.

Yo tampoco entendí completamente cómo funcionaba una función antirrebote cuando la encontré por primera vez. Aunque son relativamente pequeños, ¡en realidad emplean algunos conceptos JavaScript bastante avanzados! Tener un buen control del alcance, los cierres y el setTimeoutmétodo ayudarán.

Dicho esto, a continuación se explica y se muestra la función básica antirrebote en mi publicación mencionada anteriormente.

El producto terminado

// 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 explicación

// 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);
        }
    };
};
John Dugan
fuente
1

Lo que desea hacer es lo siguiente: si intenta llamar a una función inmediatamente después de otra, la primera debe cancelarse y la nueva debe esperar un tiempo de espera determinado y luego ejecutarse. Entonces, ¿en efecto necesita alguna forma de cancelar el tiempo de espera de la primera función? ¿Pero cómo? Usted podria llamar a la función, y pasar el tiempo de espera de regresar-id y luego pasar esa identificación en cualquier nueva función. Pero la solución anterior es mucho más elegante.

Lo que hace es que la timeoutvariable esté efectivamente disponible en el alcance de la función devuelta. Entonces, cuando se activa un evento de 'cambio de tamaño', no debounce()vuelve a llamar , por lo tanto, el timeoutcontenido no se modifica (!) Y aún está disponible para la "próxima llamada de función".

La clave aquí es básicamente que llamamos a la función interna cada vez que tenemos un evento de cambio de tamaño. Quizás sea más claro si imaginamos que todos los eventos de cambio de tamaño están en una matriz:

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

¿Ves que timeoutestá disponible para la próxima iteración? Y no hay ninguna razón, en mi opinión, para renombrar thisa contenty argumentspara args.

hermansc
fuente
"Renombrar" es absolutamente necesario. El significado thisy los argumentscambios dentro de la función de devolución de llamada setTimeout (). Tienes que guardar una copia en otro lugar o esa información se perderá.
CubicleSoft
1

Esta es una variación que siempre activa la función sin rebote la primera vez que se llama, con variables más descriptivas:

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);
    }
  }
};
usuario12484139
fuente
1

Método de rebote simple en 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))

Ejemplo de tiempo de ejecución JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

Shaikh Arbaaz
fuente
0

Función antirrebote simple: -

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));
Avadhut Thorat
fuente