Quelqu'un peut-il expliquer la fonction «anti-rebond» en Javascript

151

Je suis intéressé par la fonction "debouncing" en javascript, écrite ici: http://davidwalsh.name/javascript-debounce-function

Malheureusement, le code n'est pas expliqué assez clairement pour que je le comprenne. Quelqu'un peut-il m'aider à comprendre comment cela fonctionne (j'ai laissé mes commentaires ci-dessous). Bref je ne comprends vraiment pas comment ça marche

   // 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: L'extrait de code copié était auparavant callNowau mauvais endroit.

Startec
la source
1
Si vous appelez clearTimeoutavec quelque chose qui n'est pas un ID de minuterie valide, cela ne fait rien.
Ry-
@false, est-ce un comportement standard valide?
Pacerier
3
@Pacerier Oui, c'est dans la spécification : "Si handle n'identifie pas une entrée dans la liste des timers actifs de l' WindowTimersobjet sur lequel la méthode a été invoquée, la méthode ne fait rien."
Mattias Buelens

Réponses:

134

Le code de la question a été légèrement modifié par rapport au code du lien. Dans le lien, il y a une vérification (immediate && !timeout)AVANT de créer un nouveau délai d'attente. L'avoir après fait que le mode immédiat ne se déclenche jamais. J'ai mis à jour ma réponse pour annoter la version de travail à partir du lien.

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
la source
1
pour le immediate && timeoutchèque. N'y aura-t-il pas toujours un timeout(car il timeoutest appelé plus tôt). Aussi, à quoi sert clearTimeout(timeout)-il, quand il est déclaré (le rendant non défini) et effacé, plus tôt
Startec
La immediate && !timeoutvérification concerne le moment où l'anti-rebond est configuré avec l' immediateindicateur. Cela exécutera la fonction immédiatement mais imposera un waitdélai avant de pouvoir être exécuté à nouveau. Donc, la !timeoutpartie dit essentiellement 'désolé bub, cela a déjà été exécuté dans la fenêtre définie' ... rappelez-vous que la fonction setTimeout l'effacera, permettant au prochain appel de s'exécuter.
Malk
1
Pourquoi timeout doit-il être défini sur null à l'intérieur de la setTimeoutfonction? De plus, j'ai essayé ce code, pour moi, passer truepour immédiat empêche simplement la fonction d'être appelée du tout (plutôt que d'être appelée après un délai). Cela vous arrive-t-il?
Startec
J'ai une question similaire sur immédiate? pourquoi a-t-il besoin du paramètre immédiat. Mettre wait à 0 devrait avoir le même effet, non? Et comme @Startec l'a mentionné, ce comportement est assez étrange.
zeroliu
2
Si vous appelez simplement la fonction, vous ne pouvez pas imposer une minuterie d'attente avant que cette fonction puisse être appelée à nouveau. Pensez à un jeu où l'utilisateur écrase la clé de tir. Vous voulez que ce feu se déclenche immédiatement, mais ne se déclenche pas à nouveau pendant X millisecondes supplémentaires, quelle que soit la vitesse à laquelle l'utilisateur écrase le bouton.
Malk
57

La chose importante à noter ici est que debounceproduit une fonction qui est "fermée sur" la timeoutvariable. La timeoutvariable reste accessible à chaque appel de la fonction produite même après debounceson retour, et peut changer sur différents appels.

L'idée générale debounceest la suivante:

  1. Commencez sans délai.
  2. Si la fonction produite est appelée, effacez et réinitialisez le délai.
  3. Si le délai d'expiration est atteint, appelez la fonction d'origine.

Le premier point est juste var timeout;, c'est effectivement juste undefined. Heureusement, il clearTimeoutest assez laxiste sur son entrée: passer un undefinedidentifiant de minuterie le fait simplement ne rien faire, il ne génère pas d'erreur ou quelque chose.

Le deuxième point est fait par la fonction produite. Il stocke d'abord des informations sur l'appel (le thiscontexte et le arguments) dans des variables afin de pouvoir les utiliser ultérieurement pour l'appel déboncé. Il efface ensuite le délai (s'il y en avait un), puis en crée un nouveau pour le remplacer en utilisant setTimeout. Notez que cela écrase la valeur de timeoutet que cette valeur persiste sur plusieurs appels de fonction! Cela permet au anti-rebond de fonctionner réellement: si la fonction est appelée plusieurs fois, elle timeoutest écrasée plusieurs fois avec une nouvelle minuterie. Si ce n'était pas le cas, plusieurs appels provoqueraient le démarrage de plusieurs temporisateurs, qui resteraient tous actifs - les appels seraient simplement retardés, mais pas rejetés.

Le troisième point est fait dans le rappel de timeout. Il annule la timeoutvariable et effectue l'appel de fonction réel en utilisant les informations d'appel stockées.

Le immediatedrapeau est censé contrôler si la fonction doit être appelée avant ou après le temporisateur. Si tel est le cas false, la fonction d'origine n'est appelée qu'après avoir frappé la minuterie. Si tel est le cas true, la fonction d'origine est appelée en premier et ne sera plus appelée tant que la minuterie ne sera pas atteinte.

Cependant, je pense que la if (immediate && !timeout)vérification est erronée: timeoutvient d'être définie sur l'identificateur de minuterie renvoyé par setTimeoutdonc !timeouttoujours falseà ce point et donc la fonction ne peut jamais être appelée. La version actuelle de underscore.js semble avoir une vérification légèrement différente, où elle évalue immediate && !timeout avant d' appeler setTimeout. (L'algorithme est également un peu différent, par exemple il ne l'utilise pas clearTimeout.) C'est pourquoi vous devriez toujours essayer d'utiliser la dernière version de vos bibliothèques. :-)

Mattias Buelens
la source
"Notez que cela écrase la valeur de timeout et que cette valeur persiste sur plusieurs appels de fonction" Le timeout n'est-il pas local pour chaque appel anti-rebond? Il est déclaré avec var. Comment est-il écrasé à chaque fois? Aussi, pourquoi vérifier !timeoutà la fin? Pourquoi n'existe-t-il pas toujours (car il est réglé sursetTimeout(function() etc.)
Startec
2
@Startec Il est local à chaque appel de debounce, oui, mais il est partagé entre les appels à la fonction retournée (qui est la fonction que vous allez utiliser). Par exemple, dans g = debounce(f, 100), la valeur de timeoutpersiste sur plusieurs appels à g. La !timeoutvérification à la fin est une erreur, je crois, et ce n'est pas dans le code underscore.js actuel.
Mattias Buelens
Pourquoi le délai d'attente doit-il être effacé au début de la fonction de retour (juste après sa déclaration)? En outre, il est ensuite défini sur null à l'intérieur de la fonction setTimeout. N'est-ce pas redondant? (Tout d'abord, il est effacé, puis défini sur null. Dans mes tests avec le code ci-dessus, la définition immédiate de true empêche la fonction d'appeler du tout, comme vous l'avez mentionné. Une solution sans trait de soulignement?
Startec
34

Les fonctions rebondies ne s'exécutent pas lorsqu'elles sont appelées, elles attendent une pause des appels pendant une durée configurable avant de s'exécuter; chaque nouvel appel redémarre le minuteur.

Les fonctions limitées s'exécutent, puis attendent une durée configurable avant de pouvoir se déclencher à nouveau.

Le debounce est idéal pour les événements de pression de touche; lorsque l'utilisateur commence à taper puis s'arrête, vous soumettez toutes les pressions sur les touches en un seul événement, réduisant ainsi les appels de gestion.

Throttle est idéal pour les points de terminaison en temps réel que vous ne souhaitez autoriser l'utilisateur à appeler qu'une seule fois par période définie.

Découvrez également Underscore.js pour leurs implémentations.

jurassix
la source
25

J'ai écrit un article intitulé Demistifying Debounce en JavaScript dans lequel j'explique exactement comment fonctionne une fonction anti-rebond et j'inclus une démo.

Je n'ai pas non plus pleinement compris comment fonctionnait une fonction anti-rebond lorsque j'en ai rencontré une pour la première fois. Bien que de taille relativement petite, ils utilisent en fait des concepts JavaScript assez avancés! Avoir une bonne prise sur la portée, les fermetures et la setTimeoutméthode vous aidera.

Cela dit, vous trouverez ci-dessous la fonction anti-rebond de base expliquée et démontrée dans mon article référencé ci-dessus.

Le produit fini

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

L'explication

// 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
la source
1

Ce que vous voulez faire est ce qui suit: Si vous essayez d'appeler une fonction juste après une autre, la première doit être annulée et la nouvelle doit attendre un délai d'attente donné, puis s'exécuter. Donc, en fait, vous avez besoin d'un moyen d'annuler le délai d'expiration de la première fonction? Mais comment? Vous pouvez appeler la fonction et transmettre le timeout-id de retour, puis passer cet ID dans toutes les nouvelles fonctions. Mais la solution ci-dessus est bien plus élégante.

Cela rend effectivement la timeoutvariable disponible dans la portée de la fonction retournée. Ainsi, quand un événement 'resize' est déclenché, il n'appelle debounce()plus, donc le timeoutcontenu n'est pas modifié (!) Et toujours disponible pour le "prochain appel de fonction".

L'essentiel ici est que nous appelons la fonction interne chaque fois que nous avons un événement de redimensionnement. C'est peut-être plus clair si nous imaginons que tous les événements de redimensionnement sont dans un tableau:

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

Vous voyez que le timeoutest disponible pour la prochaine itération? Et il n'y a aucune raison, à mon avis, de renommer thisen contentet argumentsen args.

hermansc
la source
"Renommer" est absolument nécessaire. La signification thiset les argumentschangements dans la fonction de rappel setTimeout (). Vous devez conserver une copie ailleurs ou cette information est perdue.
CubicleSoft
1

Il s'agit d'une variante qui déclenche toujours la fonction non rebondie la première fois qu'elle est appelée, avec des variables nommées de manière plus descriptive:

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
la source
1

Méthode simple Debounce en 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))

Exemple d'exécution JSFiddle: https://jsfiddle.net/arbaazshaikh919/d7543wqe/10/

Cheikh Arbaaz
la source
0

Fonction anti-rebond simple: -

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
la source