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é suffisamment clairement pour que je puisse comprendre. Quelqu'un peut-il m'aider à comprendre comment cela fonctionne (j'ai laissé mes commentaires ci-dessous). En bref, je ne comprends vraiment pas comment cela fonctionne
// 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: le fragment de code copié précédemment contenait callNow
au mauvais endroit.
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 de (immediate && !timeout)
AVANT de créer un nouveau délai d'attente. Après l'avoir après, 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);
La chose importante à noter ici est que debounce
produit une fonction qui est "fermée sur" la variable timeout
. La variable timeout
reste accessible pendant chaque appel de la fonction produite même après le retour de debounce
elle-même, et peut être modifié (e) appels.
L'idée générale de debounce
est la suivante:
Le premier point est juste var timeout;
, il s'agit bien de undefined
. Heureusement, clearTimeout
est assez laxiste en ce qui concerne son entrée: le fait de passer un identifiant undefined
le fait tout simplement, il ne fait rien, il ne jette aucune erreur.
Le deuxième point est fait par la fonction produite. Il stocke d’abord des informations sur l’appel (le contexte this
et les arguments
) dans des variables afin de pouvoir les utiliser ultérieurement pour l’appel traité. Il efface ensuite le délai (s'il y en avait un), puis en crée un nouveau à remplacer par setTimeout
. Notez que ceci écrase la valeur de timeout
et que cette valeur persiste après plusieurs appels de fonction! Ceci permet à l'anti-rebond de fonctionner: si la fonction est appelé plusieurs fois, timeout
est écrasé plusieurs fois par une nouvelle minuterie. Si ce n'était pas le cas, plusieurs appels entraîneraient le démarrage de plusieurs minuteries qui resteraient actives - les appels seraient simplement retardés, mais non rejetés.
Le troisième point est fait dans le rappel de délai d'attente. Il désactive la variable timeout
et effectue l'appel de la fonction en utilisant les informations d'appel stockées.
Le drapeau immediate
est supposé contrôler si la fonction doit être appelée avant ou après la minuterie. S'il s'agit de false
, la fonction d'origine n'est appelée que après le minuteur est activé. S'il s'agit de true
, la fonction d'origine est d'abord appelée et ne sera plus appelée tant que le chronomètre n'aura pas été touché.
Cependant, j'estime que le contrôle if (immediate && !timeout)
est incorrect: timeout
vient d'être défini sur l'identificateur de temporisateur renvoyé par setTimeout
de sorte que !timeout
est toujours 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 l'appel setTimeout
. (L'algorithme est également un peu différent, par exemple, il n'utilise pas clearTimeout
.) C'est pourquoi vous devriez toujours essayer d'utiliser la dernière version de vos bibliothèques. :-)
Les fonctions débattues ne s'exécutent pas lorsqu'elles sont appelées, elles attendent une pause des invocations sur une durée configurable avant de s'exécuter. chaque nouvel appel redémarre le temporisateur.
Les fonctions limitées s'exécutent, puis attendent une durée configurable avant de pouvoir être réactivées.
Debounce est idéal pour les événements de pression de touche; lorsque l'utilisateur commence à taper puis se met en pause, vous soumettez tous les appuis sur les touches en tant qu'événement unique, ce qui réduit les invocations de traitement.
Throttle est idéal pour les systèmes d'extrémité en temps réel que vous souhaitez uniquement permettre à l'utilisateur d'appeler une fois par période définie.
Découvrez nderscore.js pour leurs implémentations aussi.
J'ai écrit un article intitulé démystifiant Debounce en JavaScript , où j'explique exactement comment fonctionne une fonction anti-rebond et inclut une démo.
Moi aussi, je n'ai pas bien compris le fonctionnement d'une fonction anti-rebond lorsque j'en ai rencontré une pour la première fois. Bien que de taille relativement petite, ils utilisent en réalité des concepts JavaScript assez avancés! Une bonne prise en main des oscilloscopes, des fermetures et de la méthode setTimeout
vous aidera.
Cela dit, vous trouverez ci-dessous la fonction de base anti-rebond expliquée et démontrée dans mon message mentionné 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);
}
};
};
Ce que vous voulez faire est le suivant: Si vous essayez d’appeler une fonction immédiatement après une autre, la première doit être annulée et la nouvelle doit attendre un délai donné, puis être exécutée. Donc, en réalité, vous avez besoin d’un moyen d’annuler la temporisation de la première fonction? Mais comment? Vous pourriez appeler la fonction et transmettre l'identifiant timeout renvoyé, puis l'identifiant dans toute nouvelle fonction. Mais la solution ci-dessus est beaucoup plus élégante.
Cela rend effectivement la variable timeout
disponible dans l'étendue de la fonction retournée. Ainsi, lorsqu'un événement 'redimensionner' est déclenché, il n'appelle pas à nouveau debounce()
. Le contenu de timeout
n'est donc pas modifié (!) Et reste disponible pour le "prochain appel de fonction".
L’essentiel ici est que nous appelions la fonction interne chaque fois que nous organisons un événement de redimensionnement. Peut-être est-il 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 la timeout
est disponible à la prochaine itération? Et il n'y a aucune raison, à mon avis, de renommer this
en content
et arguments
en args
.