Alguém pode explicar a função "debounce" em Javascript

151

Estou interessado na função "debouncing" em javascript, escrita aqui: http://davidwalsh.name/javascript-debounce-function

Infelizmente, o código não é explicado com clareza suficiente para eu entender. Alguém pode me ajudar a descobrir como funciona (deixei meus comentários abaixo). Em suma, eu realmente não entendo como isso funciona

   // 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: O trecho de código copiado anteriormente tinha callNowno lugar errado.

Startec
fonte
1
Se você ligar clearTimeoutcom algo que não seja um ID de timer válido, ele não fará nada.
Ry-
@ Falso, esse é o comportamento padrão válido?
Pacerier
3
@Pacerier Sim, está na especificação : "Se o identificador não identificar uma entrada na lista de temporizadores ativos do WindowTimersobjeto no qual o método foi chamado, o método não fará nada".
Mattias Buelens

Respostas:

134

O código na pergunta foi ligeiramente alterado do código no link. No link, há uma verificação (immediate && !timeout)antes de criar um novo tempo limite. Tê-lo depois faz com que o modo imediato nunca seja acionado. Atualizei minha resposta para anotar a versão de trabalho no 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);

Malk
fonte
1
para o immediate && timeoutcheque. Não haverá sempre um timeout(porque timeouté chamado anteriormente). Além disso, o bom é clearTimeout(timeout)fazer, quando é declarado (o que torna indefinido) e liberado, anteriormente
Startec
A immediate && !timeoutverificação é para quando o debounce é configurado com o immediatesinalizador. Isso executará a função imediatamente, mas impõe um waittempo limite antes, se puder ser executado novamente. Portanto, a !timeoutparte está basicamente dizendo 'desculpe, isso já foi executado dentro da janela definida' ... lembre-se de que a função setTimeout a limpará, permitindo que a próxima chamada seja executada.
Malk
1
Por que o tempo limite deve ser definido como nulo dentro da setTimeoutfunção? Além disso, eu tentei esse código para mim, passar truepara imediato apenas impede que a função seja chamada (em vez de ser chamada após um atraso). Isso acontece para você?
Startec
Eu tenho uma pergunta semelhante sobre imediato? por que ele precisa ter o parâmetro imediato. Definir espera para 0 deve ter o mesmo efeito, certo? E como o @Startec mencionou, esse comportamento é bastante estranho.
Zeroliu 06/07/2015
2
Se você apenas chamar a função, não poderá impor um timer de espera antes que a função possa ser chamada novamente. Pense em um jogo em que o usuário pressiona a tecla de fogo. Você deseja que esse disparo seja acionado imediatamente, mas não seja acionado novamente por mais X milissegundos, não importa a rapidez com que o usuário aperte o botão.
Malk
57

O importante a ser observado aqui é que debounceproduz uma função "fechada" sobre a timeoutvariável. A timeoutvariável permanece acessível durante todas as chamadas da função produzida, mesmo após o debounceretorno, e pode alterada em diferentes chamadas.

A ideia geral para debounceé a seguinte:

  1. Comece sem tempo limite.
  2. Se a função produzida for chamada, limpe e redefina o tempo limite.
  3. Se o tempo limite for atingido, chame a função original.

O primeiro ponto é justo var timeout;, é de fato justo undefined. Felizmente, clearTimeouté bastante relaxado quanto a sua contribuição: passar umundefined identificador de timer faz com que ele simplesmente não faça nada, não gera um erro ou algo assim.

O segundo ponto é feito pela função produzida. Primeiro, ele armazena algumas informações sobre a chamada (o thiscontexto e o arguments) em variáveis ​​para que possa usá-las posteriormente para a chamada rejeitada. Em seguida, limpa o tempo limite (se houver um conjunto) e cria um novo para substituí-lo usando setTimeout. Observe que isso substitui o valor de timeoute esse valor persiste em várias chamadas de função! Isso permite que o debounce realmente funcione: se a função for chamada várias vezes, timeoutserá substituída várias vezes por um novo timer. Se não fosse esse o caso, várias chamadas causariam o início de vários temporizadores, todos permanecendo ativos - as chamadas seriam atrasadas, mas não canceladas.

O terceiro ponto é feito no retorno de chamada de tempo limite. Desativa a timeoutvariável e realiza a chamada de função real usando as informações de chamada armazenadas.

O immediatesinalizador deve controlar se a função deve ser chamada antes ou depois do timer. Se for false, a função original não é chamada até depois que o timer for pressionado. Se for true, a função original é chamada primeiro e não será mais chamada até que o cronômetro seja acionado.

No entanto, acredito que a if (immediate && !timeout)verificação está errada: timeoutacabou de ser definida como o identificador de timer retornado por, setTimeoutportanto, !timeoutestá sempre falsenesse ponto e, portanto, a função nunca pode ser chamada. A versão atual do underscore.js parece ter uma verificação um pouco diferente, avaliada immediate && !timeout antes da chamada setTimeout. (O algoritmo também é um pouco diferente, por exemplo, não usa clearTimeout.) É por isso que você deve sempre tentar usar a versão mais recente de suas bibliotecas. :-)

Mattias Buelens
fonte
"Observe que isso substitui o valor do tempo limite e esse valor persiste em várias chamadas de função" O tempo limite não é local para cada chamada de devolução? É declarado com var. Como é substituído cada vez? Além disso, por que procurar!timeout no final? Por que não ele sempre existir (porque ela é definida comosetTimeout(function() etc.)
Startec
2
@Startec É local para cada chamada de debounce, sim, mas é compartilhado entre as chamadas para a função retornada (que é a função que você vai usar). Por exemplo, em g = debounce(f, 100), o valor de timeoutpersiste em várias chamadas para g. A !timeoutverificação no final é um erro, acredito, e não está no código underscore.js atual.
precisa saber é o seguinte
Por que o tempo limite precisa ser limpo no início da função de retorno (logo após ser declarado)? Além disso, ele é definido como nulo dentro da função setTimeout. Isso não é redundante? (Primeiro, é limpo e depois definido como null. Nos meus testes com o código acima, a configuração imediata como true faz com que a função não seja chamada, como você mencionou. Qualquer solução sem sublinhado?
Startec
34

As funções com problemas não são executadas quando invocadas, elas aguardam uma pausa de invocações por um período configurável antes de serem executadas; cada nova chamada reinicia o cronômetro.

As funções reguladas são executadas e aguardam uma duração configurável antes de serem elegíveis para disparar novamente.

Debounce é ótimo para eventos de pressionamento de tecla; quando o usuário começa a digitar e depois pausa, você envia todas as teclas pressionadas como um único evento, reduzindo assim as chamadas de manipulação.

A aceleração é ótima para terminais em tempo real que você deseja permitir que o usuário chame apenas uma vez por um período definido.

Confira também Underscore.js para as implementações.

jurassix
fonte
24

Eu escrevi um post intitulado Demistifying Debounce in JavaScript, onde explico exatamente como uma função de debounce funciona e incluo uma demonstração.

Eu também não entendi completamente como funcionava uma função de debounce quando encontrei uma. Embora sejam relativamente pequenos, eles realmente empregam alguns conceitos avançados de JavaScript! Ter uma boa noção do escopo, dos fechamentos e do setTimeoutmétodo ajudará.

Com isso dito, abaixo está a função básica de debounce explicada e demonstrada no meu post mencionado acima.

O produto acabado

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

A explicação

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

O que você deseja fazer é o seguinte: Se você tentar chamar uma função logo após a outra, a primeira deverá ser cancelada e a nova deverá aguardar um determinado tempo limite e depois executar. Então, com efeito, você precisa de alguma maneira de cancelar o tempo limite da primeira função? Mas como? Você poderia chamar a função e passar o id de tempo limite retornado e depois passar esse ID para quaisquer novas funções. Mas a solução acima é bem mais elegante.

O que ele faz é efetivamente disponibilizar a timeoutvariável no escopo da função retornada. Portanto, quando um evento 'redimensionar' é disparado, ele não chama debounce()novamente, portanto o timeoutconteúdo não é alterado (!) E ainda está disponível para a "próxima chamada de função".

A principal coisa aqui é basicamente que chamamos a função interna toda vez que temos um evento de redimensionamento. Talvez seja mais claro se imaginarmos que todos os eventos de redimensionamento estão em uma 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);
    }
}

Você vê o timeoutdisponível para a próxima iteração? E não há razão, na minha opinião, de renomear thispara contente argumentspara args.

hermansc
fonte
"Renomear" é absolutamente necessário. O significado thise as argumentsalterações dentro da função de retorno de chamada setTimeout (). Você deve manter uma cópia em outro lugar ou essa informação será perdida.
CubicleSoft 30/03/19
1

Essa é uma variação que sempre aciona a função rejeitada na primeira vez em que é chamada, com variáveis ​​nomeadas de forma mais descritiva:

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);
    }
  }
};
user12484139
fonte
1

Método Debounce simples em 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))

Exemplo de tempo de execução JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

Shaikh Arbaaz
fonte
0

Função de debounce simples: -

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
fonte