Alguém pode explicar a function “debounce” em Javascript

Estou interessado na function “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 callNow no lugar errado.

O código na questão foi ligeiramente alterado do código no link. No link, há uma verificação para (immediate && !timeout) Limite de (immediate && !timeout) ANTES de criar um novo limite de tempo. Depois de fazer isso, o modo imediato nunca é acionado. Atualizei minha resposta para anotar a versão de trabalho do 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); }; }; 

O importante a ser observado aqui é que debounce produz uma function que é “fechada” na variável timeout . A variável de timeout permanece acessível durante cada chamada da function produzida, mesmo depois de o próprio debounce ter retornado, e pode mudar em diferentes chamadas.

A ideia geral para o debounce é a seguinte:

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

O primeiro ponto é apenas var timeout; , na verdade, é apenas undefined . Felizmente, clearTimeout é razoavelmente frouxo em relação à sua input: passar um identificador de timer undefined faz com que ele simplesmente não faça nada, não gera um erro ou algo assim.

O segundo ponto é feito pela function produzida. Primeiro, armazena algumas informações sobre a chamada (o contexto e os arguments ) em variables, para que possa usá-las posteriormente na chamada debitada. Em seguida, ele limpa o tempo limite (se houver um conjunto) e cria um novo para substituí-lo usando setTimeout . Observe que isso substitui o valor do timeout e esse valor persiste em várias chamadas de function! Isso permite que o debounce realmente funcione: se a function for chamada várias vezes, o timeout será sobrescrito várias vezes com um novo timer. Se esse não fosse o caso, várias chamadas causariam a boot de vários timers, os quais permaneceriam ativos – as chamadas seriam simplesmente atrasadas, mas não debilitadas.

O terceiro ponto é feito no retorno de chamada de tempo limite. Ele desabilita a variável de timeout e faz a chamada de function real usando as informações da chamada armazenada.

O sinalizador immediate deve controlar se a function deve ser chamada antes ou depois do timer. Se for false , a function original não será chamada até que o cronômetro seja atingido. Se for true , a function original é chamada primeiro e não será chamada mais até que o cronômetro seja atingido.

No entanto, eu acredito que a verificação if (immediate && !timeout) está errada: o timeout acabou de ser definido para o identificador de timer retornado por setTimeout então !timeout é sempre false nesse ponto e, portanto, a function nunca pode ser chamada. A versão atual do underscore.js parece ter uma verificação ligeiramente diferente, onde ele avalia immediate && !timeout antes de chamar setTimeout . (O algoritmo também é um pouco diferente, por exemplo, ele não usa clearTimeout .) É por isso que você deve sempre tentar usar a versão mais recente de suas bibliotecas. 🙂

As funções debitadas não são executadas quando invocadas, elas aguardam uma pausa de invocações em uma duração configurável antes da execução; cada nova invocação reinicia o timer.

As funções limitadas são executadas e, em seguida, aguardam uma duração configurável antes de serem elegíveis para triggersr novamente.

O debounce é ótimo para events keypress; quando o usuário começa a digitar e pausa você envia todos os pressionamentos de tecla como um único evento, reduzindo assim as chamadas de manipulação.

Throttle é ótimo para endpoints em tempo real que você só quer permitir que o usuário invoque uma vez por um período de tempo definido.

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

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

Eu também não entendi completamente como funcionava uma function de debounce quando a encontrei pela primeira vez. Embora relativamente pequenos em tamanho, eles realmente empregam alguns conceitos JavaScript bastante avançados! Ter uma boa aderência no escopo, fechamento e o método setTimeout ajudará.

Com isso dito, abaixo está a function de debounce básica explicada e demoed no meu post referenciado 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); } }; }; 

O que você quer fazer é o seguinte: Se você tentar chamar uma function logo após a outra, a primeira deve ser cancelada e a nova deve esperar por um tempo limite e então executar. Então, de fato, você precisa de algum modo de cancelar o tempo limite da primeira function? Mas como? Você poderia chamar a function, e passar o timeout-id de retorno e, em seguida, passá-lo para qualquer nova function. Mas a solução acima é bem mais elegante.

O que ele faz é efetivamente disponibilizar a variável timeout no escopo da function retornada. Portanto, quando um evento ‘resize’ é acionado, ele não chama debounce() novamente, portanto, o conteúdo de timeout não é alterado (!) E ainda está disponível para a “próxima chamada de function”.

A principal coisa aqui é basicamente que nós chamamos a function interna toda vez que temos um evento de redimensionamento. Talvez seja mais claro se imaginamos que todos os events 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ê que o timeout está disponível para a próxima iteração? E não há razão, na minha opinião, para renomear this para content e arguments para args .