Czy ktoś może wyjaśnić funkcję „debounce” w JavaScript

151

Interesuje mnie funkcja "debouncing" w javascript, napisana tutaj: http://davidwalsh.name/javascript-debounce-function

Niestety kod nie jest wystarczająco jasny, abym mógł go zrozumieć. Czy ktoś może mi pomóc dowiedzieć się, jak to działa (zostawiłem swoje komentarze poniżej). Krótko mówiąc, po prostu naprawdę nie rozumiem, jak to działa

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

EDYCJA: skopiowany fragment kodu znajdował się wcześniej callNoww niewłaściwym miejscu.

Startec
źródło
1
Jeśli dzwonisz clearTimeoutz czymś, co nie jest prawidłowym identyfikatorem timera, nic nie robi.
Ry-
@false, czy to prawidłowe standardowe zachowanie?
Pacerier
3
@Pacerier Tak, jest w specyfikacji : "Jeśli uchwyt nie identyfikuje wpisu na liście aktywnych liczników czasu WindowTimersobiektu, dla którego metoda została wywołana, metoda nic nie robi."
Mattias Buelens

Odpowiedzi:

134

Kod w pytaniu został nieznacznie zmieniony w stosunku do kodu w linku. W linku znajduje się czek (immediate && !timeout)PRZED utworzeniem nowego limitu czasu. Posiadanie go po powoduje, że tryb natychmiastowy nigdy nie uruchamia się. Zaktualizowałem moją odpowiedź, aby dodać adnotację do wersji roboczej z linku.

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
źródło
1
do immediate && timeoutsprawdzenia. Czy nie zawsze będzie timeout(ponieważ timeoutjest nazywany wcześniej). Poza tym, co dobrego robi clearTimeout(timeout), kiedy zostanie zadeklarowane (czyniąc to niezdefiniowanym) i wyczyszczone, wcześniej
Startec
immediate && !timeoutWyboru jest, gdy nieczułości jest skonfigurowany z immediateflagą. Spowoduje to natychmiastowe wykonanie funkcji, ale nałoży waitlimit czasu, zanim będzie można wykonać ponownie. Tak więc !timeoutczęść w zasadzie mówi „przepraszam, koleś, to zostało już wykonane w zdefiniowanym oknie” ... pamiętaj, że funkcja setTimeout wyczyści to, pozwalając na wykonanie następnego wywołania.
Malk
1
Dlaczego limit czasu musi być ustawiony na null w setTimeoutfunkcji? Ponadto wypróbowałem ten kod, dla mnie przekazanie truenatychmiastowego po prostu zapobiega w ogóle wywołaniu funkcji (zamiast wywoływania z opóźnieniem). Czy to się dzieje dla Ciebie?
Startec
Mam podobne pytanie dotyczące natychmiastowego? dlaczego musi mieć natychmiastowy parametr. Ustawienie wait na 0 powinno mieć ten sam efekt, prawda? Jak wspomniał @Startec, takie zachowanie jest dość dziwne.
zeroliu
2
Jeśli po prostu wywołasz funkcję, nie możesz narzucić licznika czasu oczekiwania przed ponownym wywołaniem tej funkcji. Pomyśl o grze, w której użytkownik wciska klawisz ognia. Chcesz, aby ten ogień wyzwalał się natychmiast, ale nie strzelał ponownie przez kolejne X milisekund, bez względu na to, jak szybko użytkownik wciska przycisk.
Malk,
57

Ważną rzeczą do zapamiętania jest to, że debouncetworzy funkcję, która jest „zamknięta” na timeoutzmiennej. Te timeoutzmienne pobyty dostępne podczas każdej rozmowy wytworzonego funkcji nawet po debouncesam powrócił, i może ulegać zmianom w różnych połączeń.

Ogólny pomysł debouncejest następujący:

  1. Zacznij bez limitu czasu.
  2. Jeśli wywołana zostanie utworzona funkcja, wyczyść i zresetuj limit czasu.
  3. Jeśli limit czasu zostanie osiągnięty, wywołaj oryginalną funkcję.

Pierwsza kwestia jest sprawiedliwa var timeout;, rzeczywiście jest sprawiedliwa undefined. Na szczęście clearTimeoutjest dość luźny, jeśli chodzi o dane wejściowe: przekazanie undefinedidentyfikatora timera powoduje, że po prostu nic nie robi, nie zgłasza błędu ani czegoś takiego.

Drugi punkt dotyczy utworzonej funkcji. Najpierw przechowuje niektóre informacje o wywołaniu ( thiskontekst i arguments) w zmiennych, dzięki czemu może później użyć ich w wywołaniu zdemontowanym. Następnie czyści limit czasu (jeśli był jeden zestaw), a następnie tworzy nowy, aby go zastąpić za pomocą setTimeout. Zauważ, że powoduje to nadpisanie wartości timeouti ta wartość będzie trwała przez wiele wywołań funkcji! Dzięki temu debounce faktycznie działa: jeśli funkcja jest wywoływana wiele razy, timeoutjest wielokrotnie nadpisywana nowym zegarem. Gdyby tak nie było, wiele wywołań spowodowałoby uruchomienie wielu timerów, z których wszystkie pozostałyby aktywne - wywołania byłyby po prostu opóźnione, ale nie byłyby odrzucane.

Trzeci punkt jest wykonywany w wywołaniu zwrotnym limitu czasu. Cofa timeoutzmienną i wykonuje rzeczywiste wywołanie funkcji przy użyciu przechowywanych informacji o wywołaniu.

immediateFlaga ma kontrolować, czy funkcja powinna zostać wywołana przed lub po timera. Jeśli tak false, oryginalna funkcja jest wywoływana dopiero po uderzeniu licznika czasu. Jeśli tak jest true, pierwotna funkcja jest najpierw wywoływana i nie będzie wywoływana więcej, dopóki zegar nie zostanie uderzony.

Uważam jednak, że if (immediate && !timeout)sprawdzenie jest błędne: timeoutwłaśnie został ustawiony na identyfikator zegara zwrócony przez, setTimeoutwięc !timeoutjest zawsze falsew tym momencie, a zatem funkcja nigdy nie może zostać wywołana. Obecna wersja underscore.js wydaje się mieć nieco inną kontrolę, gdzie ocenia immediate && !timeout przed wywołaniem setTimeout. (Algorytm też jest trochę inny, np. Nie używa clearTimeout.) Dlatego zawsze powinieneś starać się używać najnowszej wersji swoich bibliotek. :-)

Mattias Buelens
źródło
„Zwróć uwagę, że spowoduje to nadpisanie wartości limitu czasu, a wartość ta będzie się utrzymywać przez wiele wywołań funkcji”. Czy limit czasu nie jest lokalny dla każdego wywołania odrzucenia? Jest deklarowane za pomocą var. Jak za każdym razem jest nadpisywany? Poza tym po !timeoutco sprawdzać na końcu? Dlaczego nie istnieje zawsze (ponieważ jest ustawiony nasetTimeout(function() etc.)
Startec
2
@Startec Jest lokalna dla każdego wywołania debounce, tak, ale jest współdzielona przez wywołania funkcji zwracanej (która jest funkcją, której będziesz używać). Na przykład w g = debounce(f, 100)programie wartość timeoututrzymuje się w przypadku wielu wywołań funkcji g. !timeoutCzek na końcu jest błędem wierzę, i nie jest w obecnym kodem underscore.js.
Mattias Buelens
Dlaczego limit czasu musi zostać wyczyszczony wcześnie w funkcji return (zaraz po jego zadeklarowaniu)? Ponadto jest następnie ustawiana na null wewnątrz funkcji setTimeout. Czy to nie jest zbędne? (Najpierw jest wyczyszczony, a następnie ustawiony na null. W moich testach z powyższym kodem ustawienie natychmiastowe na true powoduje, że funkcja w ogóle nie wywołuje, jak wspomniałeś. Jakiekolwiek rozwiązanie bez podkreślenia?
Startec
34

Wywołane funkcje nie są wykonywane po wywołaniu, przed wykonaniem czekają na pauzę wywołań przez konfigurowalny czas trwania; każde nowe wywołanie restartuje licznik czasu.

Ograniczone funkcje są wykonywane, a następnie czekają przez określony czas, zanim będą mogły ponownie uruchomić.

Odbicie świetnie nadaje się do wydarzeń związanych z naciśnięciem klawisza; kiedy użytkownik zacznie pisać, a następnie przerwie, wysyłasz wszystkie naciśnięcia klawiszy jako jedno zdarzenie, zmniejszając w ten sposób obsługę wywołań.

Throttle świetnie sprawdza się w przypadku punktów końcowych czasu rzeczywistego, w przypadku których użytkownik chce zezwolić użytkownikowi na wywoływanie tylko raz na określony czas.

Sprawdź także Underscore.js dla ich implementacji.

jurassix
źródło
24

Napisałem post zatytułowany Demistifying Debounce in JavaScript, w którym wyjaśniam dokładnie, jak działa funkcja debounce i dołączam demonstrację.

Ja też nie do końca rozumiałem, jak działa funkcja odbicia, kiedy pierwszy raz ją spotkałem. Chociaż są stosunkowo małe, w rzeczywistości wykorzystują dość zaawansowane koncepcje JavaScript! Pomocne będzie dobre trzymanie się lunety, zamknięć i setTimeoutmetody.

Powiedziawszy to, poniżej jest wyjaśniona i pokazana podstawowa funkcja debounce w moim poście, o którym mowa powyżej.

Skończony produkt

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

Wyjaśnienie

// 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
źródło
1

To, co chcesz zrobić, jest następujące: Jeśli próbujesz wywołać funkcję zaraz po drugiej, pierwsza powinna zostać anulowana, a nowa powinna poczekać na określony czas, a następnie wykonać. Więc w efekcie potrzebujesz jakiegoś sposobu na anulowanie limitu czasu pierwszej funkcji? Ale jak? Państwo mogłoby wywołać funkcję i przechodzą powracającego Timeout-id, a następnie przekazać ten identyfikator do nowych funkcji. Ale powyższe rozwiązanie jest o wiele bardziej eleganckie.

Skutecznie udostępnia timeoutzmienną w zakresie zwracanej funkcji. Tak więc, gdy uruchamiane jest zdarzenie „resize”, nie jest ono wywoływane debounce()ponownie, dlatego timeoutzawartość nie jest zmieniana (!) I nadal jest dostępna dla „następnego wywołania funkcji”.

Kluczową rzeczą jest to, że wywołujemy funkcję wewnętrzną za każdym razem, gdy mamy zdarzenie zmiany rozmiaru. Być może będzie to bardziej jasne, jeśli wyobrazimy sobie, że wszystkie zdarzenia zmiany rozmiaru są w tablicy:

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

Widzisz, że timeoutjest dostępny do następnej iteracji? Moim zdaniem nie ma powodu, aby zmieniać nazwę thisna contenti argumentsna args.

hermansc
źródło
„Zmiana nazwy” jest absolutnie konieczna. Znaczenie thisi argumentszmienia się wewnątrz funkcji wywołania zwrotnego setTimeout (). Musisz zachować kopię w innym miejscu lub ta informacja zostanie utracona.
CubicleSoft
1

Jest to odmiana, która zawsze uruchamia zdemontowaną funkcję przy pierwszym wywołaniu, z bardziej opisowymi nazwami zmiennych:

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
źródło
1

Prosta metoda Debounce w 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))

Przykład wykonania JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

Shaikh Arbaaz
źródło
0

Prosta funkcja odbicia: -

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
źródło