Can someone explain the "debounce" function in Javascript
Debounced functions do not execute when invoked, they wait for a pause of invocations over a configurable duration before executing; each new invocation restarts the timer.
Throttled functions execute and then wait a configurable duration before being eligible to fire again.
Debounce is great for keypress events; when the user starts typing and then pauses you submit all the key presses as a single event, thus cutting down on the handling invocations.
Throttle is great for realtime endpoints that you only want to allow the user to invoke once per a set period of time.
Check out Underscore.js for their implementations too.
The code in the question was altered slightly from the code in the link. In the link, there is a check for (immediate && !timeout)
BEFORE creating a new timout. Having it after causes immediate mode to never fire. I have updated my answer to annotate the working version from the 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);
The important thing to note here is that debounce
produces a function that is "closed over" the timeout
variable. The timeout
variable stays accessible during every call of the produced function even after debounce
itself has returned, and can change over different calls.
The general idea for debounce
is the following:
- Start with no timeout.
- If the produced function is called, clear and reset the timeout.
- If the timeout is hit, call the original function.
The first point is just var timeout;
, it is indeed just undefined
. Luckily, clearTimeout
is fairly lax about its input: passing an undefined
timer identifier causes it to just do nothing, it doesn't throw an error or something.
The second point is done by the produced function. It first stores some information about the call (the this
context and the arguments
) in variables so it can later use these for the debounced call. It then clears the timeout (if there was one set) and then creates a new one to replace it using setTimeout
. Note that this overwrites the value of timeout
and this value persists over multiple function calls! This allows the debounce to actually work: if the function is called multiple times, timeout
is overwritten multiple times with a new timer. If this were not the case, multiple calls would cause multiple timers to be started which all remain active - the calls would simply be delayed, but not debounced.
The third point is done in the timeout callback. It unsets the timeout
variable and does the actual function call using the stored call information.
The immediate
flag is supposed to control whether the function should be called before or after the timer. If it is false
, the original function is not called until after the timer is hit. If it is true
, the original function is first called and will not be called any more until the timer is hit.
However, I do believe that the if (immediate && !timeout)
check is wrong: timeout
has just been set to the timer identifier returned by setTimeout
so !timeout
is always false
at that point and thus the function can never be called. The current version of underscore.js seems to have a slightly different check, where it evaluates immediate && !timeout
before calling setTimeout
. (The algorithm is also a bit different, e.g. it doesn't use clearTimeout
.) That's why you should always try to use the latest version of your libraries. :-)