Kann jemand die "Entprellungs" -Funktion in Javascript erklären


150

Ich interessiere mich für die Funktion "Entprellen" in Javascript, die hier geschrieben steht: http://davidwalsh.name/javascript-debounce-function

Leider ist der Code nicht klar genug erklärt, damit ich ihn verstehen kann. Kann mir jemand helfen, herauszufinden, wie es funktioniert (ich habe meine Kommentare unten hinterlassen). Kurz gesagt, ich verstehe einfach nicht wirklich, wie das funktioniert

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

BEARBEITEN: Das kopierte Code-Snippet befand sich zuvor callNowan der falschen Stelle.


1
Wenn Sie clearTimeoutmit etwas anrufen , das keine gültige Timer-ID ist, wird nichts unternommen.
Ry-

@false, ist das gültiges Standardverhalten?
Pacerier

3
@Pacerier Ja, es steht in der Spezifikation : "Wenn das Handle keinen Eintrag in der Liste der aktiven Zeitgeber des WindowTimersObjekts identifiziert, für das die Methode aufgerufen wurde, führt die Methode nichts aus."
Mattias Buelens

Antworten:


134

Der Code in der Frage wurde geringfügig gegenüber dem Code im Link geändert. In dem Link wird (immediate && !timeout)geprüft, BEVOR Sie ein neues Zeitlimit erstellen. Wenn Sie es nachher haben, wird der Sofortmodus niemals ausgelöst. Ich habe meine Antwort aktualisiert, um die Arbeitsversion über den Link zu kommentieren.

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


1
für den immediate && timeoutScheck. Wird es nicht immer eine geben timeout(weil timeoutfrüher aufgerufen wird). Was nützt clearTimeout(timeout)es auch, wenn es früher deklariert (undefiniert) und gelöscht wird
Startec

Die immediate && !timeoutPrüfung erfolgt, wenn das Entprellen mit dem immediateFlag konfiguriert ist . Dadurch wird die Funktion sofort ausgeführt, es wird jedoch eine waitZeitüberschreitung festgelegt, bevor sie erneut ausgeführt werden kann. Der !timeoutTeil sagt also im Grunde "Entschuldigung, Bub, dies wurde bereits innerhalb des definierten Fensters ausgeführt". Denken Sie daran, dass die Funktion setTimeout es löscht und den nächsten Aufruf ausführen kann.
Malk

1
Warum muss das Timeout innerhalb der setTimeoutFunktion auf null gesetzt werden? Außerdem habe ich diesen Code für mich ausprobiert. Durch die truesofortige Übergabe wird lediglich verhindert, dass die Funktion überhaupt aufgerufen wird (anstatt nach einer Verzögerung aufgerufen zu werden). Passiert das für dich?
Startec

Ich habe eine ähnliche Frage zu sofort? Warum muss es den unmittelbaren Parameter haben? Das Setzen von wait auf 0 sollte den gleichen Effekt haben, oder? Und wie @Startec erwähnt hat, ist dieses Verhalten ziemlich seltsam.
Zeroliu

2
Wenn Sie nur die Funktion aufrufen, können Sie keinen Wartezeit-Timer festlegen, bevor diese Funktion erneut aufgerufen werden kann. Stellen Sie sich ein Spiel vor, bei dem der Benutzer den Feuerschlüssel zerdrückt. Sie möchten, dass das Feuer sofort ausgelöst wird, aber für weitere X Millisekunden nicht erneut ausgelöst wird, unabhängig davon, wie schnell der Benutzer die Taste drückt.
Malk

57

Das Wichtigste dabei ist, dass debounceeine Funktion erzeugt wird, die über der timeoutVariablen "geschlossen" ist. Die timeoutVariable bleibt bei jedem Aufruf der erzeugten Funktion auch nach ihrer debounceRückkehr zugänglich und kann über verschiedene Aufrufe wechseln.

Die allgemeine Idee für debounceist die folgende:

  1. Beginnen Sie ohne Zeitüberschreitung.
  2. Wenn die erzeugte Funktion aufgerufen wird, löschen Sie das Timeout und setzen Sie es zurück.
  3. Wenn das Timeout erreicht ist, rufen Sie die ursprüngliche Funktion auf.

Der erste Punkt ist gerecht var timeout;, es ist in der Tat gerecht undefined. Glücklicherweise clearTimeoutist die Eingabe ziemlich lasch: Wenn Sie eine undefinedTimer- ID übergeben, wird nichts unternommen, es wird kein Fehler oder ähnliches ausgegeben.

Der zweite Punkt wird von der erzeugten Funktion erledigt. Es speichert zunächst einige Informationen über den Aufruf (den thisKontext und den arguments) in Variablen, damit diese später für den entprellten Anruf verwendet werden können. Es löscht dann das Zeitlimit (falls es einen Satz gab) und erstellt dann einen neuen, um ihn durch zu ersetzen setTimeout. Beachten Sie, dass dies den Wert von überschreibt timeoutund dieser Wert bei mehreren Funktionsaufrufen bestehen bleibt! Dadurch kann das Entprellen tatsächlich funktionieren: Wenn die Funktion mehrmals aufgerufen wird, timeoutwird sie mehrmals mit einem neuen Timer überschrieben. Wenn dies nicht der Fall wäre, würden mehrere Anrufe dazu führen, dass mehrere Timer gestartet werden, die alle aktiv bleiben - die Anrufe würden einfach verzögert, aber nicht entprellt.

Der dritte Punkt wird im Timeout-Rückruf erledigt. Es setzt die timeoutVariable zurück und führt den eigentlichen Funktionsaufruf unter Verwendung der gespeicherten Aufrufinformationen aus .

Das immediateFlag soll steuern, ob die Funktion vor oder nach dem Timer aufgerufen werden soll . Wenn dies der Fall ist false, wird die ursprüngliche Funktion erst aufgerufen, nachdem der Timer gedrückt wurde. Wenn dies trueder Fall ist , wird die ursprüngliche Funktion zuerst aufgerufen und erst dann aufgerufen, wenn der Timer erreicht ist.

Ich glaube jedoch, dass die if (immediate && !timeout)Prüfung falsch ist: timeoutWurde gerade auf die von zurückgegebene Timer-ID gesetzt setTimeout, !timeoutist also immer falsean diesem Punkt und daher kann die Funktion niemals aufgerufen werden. Die aktuelle Version von underscore.js scheint eine etwas andere Prüfung zu haben, bei der sie immediate && !timeout vor dem Aufruf ausgewertet wird setTimeout. (Der Algorithmus ist auch etwas anders, z. B. wird er nicht verwendet clearTimeout.) Deshalb sollten Sie immer versuchen, die neueste Version Ihrer Bibliotheken zu verwenden. :-)


"Beachten Sie, dass dadurch der Wert des Zeitlimits überschrieben wird und dieser Wert bei mehreren Funktionsaufrufen bestehen bleibt." Ist das Zeitlimit nicht bei jedem Entprellungsaufruf lokal? Es wird mit var deklariert. Wie wird es jedes Mal überschrieben? Warum !timeoutam Ende nachsehen? Warum existiert es nicht immer (weil es aufsetTimeout(function() etc.)
Startec

2
@Startec Es ist lokal für jeden Aufruf von debounce, ja, aber es wird von Aufrufen der zurückgegebenen Funktion (die die Funktion ist, die Sie verwenden werden) geteilt. In bleibt g = debounce(f, 100)der Wert von beispielsweise bei timeoutmehreren Aufrufen von bestehen g. Die !timeoutÜberprüfung am Ende ist meines Erachtens ein Fehler und nicht im aktuellen Code von underscore.js enthalten.
Mattias Buelens

Warum muss das Timeout frühzeitig in der Rückgabefunktion gelöscht werden (direkt nachdem es deklariert wurde)? Außerdem wird es innerhalb der Funktion setTimeout auf null gesetzt. Ist das nicht überflüssig? (Zuerst wird es gelöscht, dann wird es gesetzt null. In meinen Tests mit dem obigen Code wird die Funktion, wenn Sie sofort auf true setzen, überhaupt nicht aufgerufen, wie Sie erwähnt haben. Irgendeine Lösung ohne Unterstrich?
Startec

34

Entprellte Funktionen werden beim Aufrufen nicht ausgeführt. Sie warten vor der Ausführung über eine konfigurierbare Dauer auf eine Aufrufpause. Jeder neue Aufruf startet den Timer neu.

Gedrosselte Funktionen werden ausgeführt und warten eine konfigurierbare Dauer, bevor sie erneut ausgelöst werden können.

Debounce eignet sich hervorragend für Tastendruckereignisse. Wenn der Benutzer mit der Eingabe beginnt und dann pausiert, senden Sie alle Tastendrücke als einzelnes Ereignis, wodurch die Bearbeitungsaufrufe reduziert werden.

Throttle eignet sich hervorragend für Echtzeit-Endpunkte, die der Benutzer nur einmal pro festgelegten Zeitraum aufrufen darf.

Schauen Sie sich auch Underscore.js für ihre Implementierungen an.


24

Ich habe einen Beitrag mit dem Titel Demistifying Debounce in JavaScript geschrieben, in dem ich genau erkläre, wie eine Debounce-Funktion funktioniert, und eine Demo einbinde .

Auch ich habe nicht ganz verstanden, wie eine Entprellungsfunktion funktioniert, als ich zum ersten Mal auf eine gestoßen bin. Obwohl sie relativ klein sind, verwenden sie tatsächlich einige ziemlich fortgeschrittene JavaScript-Konzepte! Ein guter Überblick über Umfang, Verschlüsse und die setTimeoutMethode hilft dabei.

Nachstehend wird die grundlegende Entprellungsfunktion erläutert und in meinem oben genannten Beitrag vorgeführt.

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

Die Erklärung

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

1

Was Sie tun möchten, ist Folgendes: Wenn Sie versuchen, eine Funktion direkt nach der anderen aufzurufen, sollte die erste abgebrochen werden und die neue sollte auf eine bestimmte Zeitüberschreitung warten und dann ausgeführt werden. Sie benötigen also eine Möglichkeit, das Timeout der ersten Funktion abzubrechen? Aber wie? Sie können die Funktion aufrufen, die zurückgegebene Timeout-ID übergeben und diese ID dann an neue Funktionen übergeben. Aber die obige Lösung ist viel eleganter.

Dadurch wird die timeoutVariable effektiv im Bereich der zurückgegebenen Funktion verfügbar gemacht. Wenn also ein 'resize'-Ereignis ausgelöst wird, wird es nicht debounce()erneut aufgerufen , daher wird der timeoutInhalt nicht geändert (!) Und ist weiterhin für den "nächsten Funktionsaufruf" verfügbar.

Das Wichtigste dabei ist, dass wir die interne Funktion jedes Mal aufrufen, wenn wir ein Größenänderungsereignis haben. Vielleicht ist es klarer, wenn wir uns vorstellen, dass sich alle Größenänderungsereignisse in einem Array befinden:

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

Sie sehen, dass timeoutfür die nächste Iteration verfügbar ist? Und es gibt keinen Grund, meiner Meinung nach Umbenennungs thiszu contentund argumentszu args.


"Umbenennen" ist unbedingt erforderlich. Die Bedeutung thisund argumentsÄnderungen innerhalb der Rückruffunktion setTimeout (). Sie müssen eine Kopie an anderer Stelle aufbewahren, sonst gehen diese Informationen verloren.
CubicleSoft

1

Dies ist eine Variante, die die entprellte Funktion beim ersten Aufruf immer mit aussagekräftigeren Variablen auslöst:

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

1

Einfache Debounce-Methode in 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))

Laufzeitbeispiel JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/


0

Einfache Entprellfunktion: -

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));
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.