debouncing - Ce este funcția „debounce” în JavaScript?


Sunt interesat de funcția „debouncing” în JavaScript, la Funcția Debounce în JavaScript.

Din păcate, codul nu este explicat suficient de clar pentru ca eu să înțeleg. Cum funcționează (am lăsat comentariile mele mai jos)? Pe scurt, chiar nu înțeleg cum funcționează aceasta.

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

Fragmentul de cod copiat avea anterior callNow în locul greșit.

Răspuns #1

Codul din întrebare a fost modificat ușor față de codul din link. În link, există o verificare pentru (immediate && !timeout) înainte de a crea un nou timeout. Presupunând că este după, modul imediat nu se execută niciodată. Mi-am actualizat răspunsul pentru a nota versiunea funcțională din 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);

Răspunsul #2

Lucrul important de reținut aici este că debounce produce o funcție care este „închisă peste” variabila timeout. Variabila timeout rămâne accesibilă în timpul fiecărei apelări a funcției produse chiar și după ce debounce însuși s-a întors, și poate să se schimbe în timpul diferitelor apeluri.

Ideea generală pentru debounce este următoarea:

  1. Începe fără limită de timp.
  2. Dacă funcția produsă este apelată, ștergeți și resetați temporizatorul.
  3. Dacă timpul expiră, apelează funcția originală.

Primul punct este doar var timeout;, este într-adevăr doar undefined. Din fericire, clearTimeout este destul de permisiv în privința inputului său: trecerea unui identificator de timer undefined face ca acesta să nu facă nimic, nu aruncă o eroare sau ceva similar.

Al doilea punct este realizat de funcția produsă. Aceasta stochează mai întâi câteva informații despre apel (contextul this și arguments) în variabile, astfel încât să le poată folosi ulterior pentru apelul debounced. Apoi, anulează timeout-ul (dacă a fost setat unul) și creează unul nou pentru a-l înlocui folosind setTimeout. Rețineți că acest lucru suprascrie valoarea lui timeout și această valoare persistă pe parcursul mai multor apeluri ale funcției! Acest lucru permite debounce-ului să funcționeze efectiv: dacă funcția este apelată de mai multe ori, timeout este suprascris de mai multe ori cu un nou timer. Dacă nu ar fi așa, apelurile multiple ar cauza pornirea mai multor timere, care toate rămân active - apelurile ar fi doar întârziate, dar nu debounced.

Al treilea punct se realizează în apelul de întârziere (timeout). Acesta dezactivează variabila timeout și efectuează apelul efectiv al funcției folosind informațiile de apel stocate.

Flagul immediate ar trebui să controleze dacă funcția ar trebui apelată înainte sau după timer. Dacă este false, funcția originală nu este apelată decât după ce timerul a expirat. Dacă este true, funcția originală este apelată mai întâi și nu va mai fi apelată până când timerul expiră.

Totuși, cred că verificarea if (immediate && !timeout) este greșită: timeout tocmai a fost setat la identificatorul temporizatorului returnat de setTimeout, așadar !timeout este întotdeauna false în acel moment și, prin urmare, funcția nu poate fi niciodată apelată. Versiunea actuală a underscore.js pare să aibă o verificare puțin diferită, unde evaluează immediate && !timeout înainte de a apela setTimeout. (Algoritmul este, de asemenea, puțin diferit, de exemplu nu folosește clearTimeout.) De aceea ar trebui să încerci întotdeauna să folosești cea mai recentă versiune a bibliotecilor tale. :-)

Răspunsul #3

Funcțiile cu debounce nu se execută atunci când sunt apelate. Ele așteaptă o pauză în apeluri pe o durată configurabilă înainte de a se executa; fiecare apel nou repornește temporizatorul.

Funcțiile limitate execută și apoi așteaptă o durată configurabilă înainte de a fi eligibile să fie declanșate din nou.

Debounce este grozav pentru evenimentele de apăsare a tastelor; când utilizatorul începe să tasteze și apoi se oprește, trimiteți toate apăsările de taste ca un singur eveniment, reducând astfel numărul de invocări ale handler-ului.

Throttle este grozav pentru punctele finale în timp real pe care doriți să permiteți utilizatorului să le invoce doar o dată la un interval de timp stabilit.

Verifică și Underscore.js pentru implementările lor.

Răspunsul #4

Nici eu nu am înțeles pe deplin cum funcționează o funcție debounce când am întâlnit una pentru prima dată. Deși sunt relativ mici ca dimensiune, ele folosesc de fapt câteva concepte JavaScript destul de avansate! Să ai o bună înțelegere a scope-ului, a închiderilor și a metodei setTimeout te va ajuta.

Spunând asta, mai jos este funcția de debounce de bază explicată și demonstrată în postul meu menționat mai sus.

Produsul finit

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

Explicația

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

Răspunsul #5

toți folosim Promisiuni acum

Multe implementări pe care le-am văzut complică excesiv problema sau au alte probleme de igienă. Este 2021 și folosim Promises de mult timp - și pe bună dreptate. Promises curăță programele asincrone și reduc oportunitățile ca greșelile să apară. În această postare vom scrie propriul nostru debounce. Această implementare va -

  • a avea cel mult o promisiune în așteptare în orice moment (pe fiecare sarcină debounced)
  • oprește scurgerile de memorie prin anularea corectă a promisiunilor în așteptare
  • rezolvă doar ultima promisiune
  • demonstrează comportamentul corect cu demonstrații de cod live

Scriem debounce cu cei doi parametri ai săi, task de debouncer și cantitatea de milisecunde pentru întârziere, ms. Introducem o singură legătură locală pentru starea sa locală, t -

function debounce (task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return async (...args) => {
    try {
      t.cancel()
      t = deferred(ms)
      await t.promise
      await task(...args)
    }
    catch (_) { /* prevent memory leak */ }
  }
}

Ne bazăm pe o funcție deferred reutilizabilă, care creează o nouă promisiune ce se rezolvă în ms milisecunde. Aceasta introduce două legături locale, însăși promisiunea și posibilitatea de a o anula -

function deferred (ms) {
  let cancel, promise = new Promise((resolve, reject) => {
    cancel = reject
    setTimeout(resolve, ms)
  })
  return { promise, cancel }
}

exemplu de contor de clicuri

În acest prim exemplu, avem un buton care numără clicurile utilizatorului. Evenimentul ascultător este atașat folosind debounce, astfel încât contorul este incrementat doar după o durată specificată -

// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }

// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter

// event handler
function clickCounter (event) {
  mycounter.value = Number(mycounter.value) + 1
}

// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>

exemplu de interogare live, "autocompletare"

În acest al doilea exemplu, avem un formular cu un câmp de introducere a textului. Interogarea noastră search este atașată folosind debounce -

// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }

// dom references
const myform = document.forms.myform
const myresult = myform.myresult

// event handler
function search (event) {
  myresult.value = `Searching for: ${event.target.value}`
}

// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))
<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>

multiple dempieri, hook-ul React useDebounce

Într-o altă sesiune de întrebări și răspunsuri, cineva întreabă dacă este posibil să expui mecanismul de anulare a debounce-ului și să creezi un hook React useDebounce. Folosind deferred de mai sus, este un exercițiu trivial.

// revised implementation
function debounce(task, ms) {
  let t = { promise: null, cancel: _ => void 0 }
  return [
   // ...,
    _ => t.cancel() // ✅ return cancellation mechanism
  ]
}
// revised usage
const [inc, cancel] = debounce(clickCounter, 1000) // ✅ two controls
myform.mybutton.addEventListener("click", inc)
myform.mycancel.addEventListener("click", cancel)

Implementarea unui useDebounce hook în React este floare la ureche -

function useDebounce(task, ms) {
  const [f, cancel] = debounce(task, ms)
  useEffect(_ => cancel) // ✅ auto-cancel when component unmounts
  return [f, cancel]
}

Mergi la întrebările și răspunsurile originale pentru o demonstrație completă