Can someone explain the "debounce" function in Javascript
Solution 1
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);
Solution 2
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. :-)
Solution 3
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.
Solution 4
I wrote a post titled Demistifying Debounce in JavaScript where I explain exactly how a debounce function works and include a demo.
I too didn't fully understand how a debounce function worked when I first encountered one. Although relatively small in size, they actually employ some pretty advanced JavaScript concepts! Having a good grip on scope, closures and the setTimeout
method will help.
With that said, below is the basic debounce function explained and demoed in my post referenced above.
The finished product
// 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);
}
};
};
The explanation
// 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);
}
};
};
Solution 5
we're all using Promises now
Many implementations I've seen over-complicate the problem or have other hygiene issues. It's 2021 and we've been using Promises for a long time now – and for good reason, too. Promises clean up asynchronous programs and reduce the opportunities for mistakes to happen. In this post we will write our own debounce
. This implementation will -
- have at most one promise pending at any given time (per debounced task)
- stop memory leaks by properly cancelling pending promises
- resolve only the latest promise
- demonstrate proper behaviour with live code demos
We write debounce
with its two parameters, the task
to debounce, and the amount of milliseconds to delay, ms
. We introduce a single local binding for its local state, t
-
function debounce (task, ms) {
let t = { promise: null, cancel: _ => void 0 }
return async (...args) => {
try {
t.cancel()
t = deferred(ms)
await t.promise
await task(...args)
}
catch (_) { /* prevent memory leak */ }
}
}
We depend on a reusable deferred
function, which creates a new promise that resolves in ms
milliseconds. It introduces two local bindings, the promise
itself, an the ability to cancel
it -
function deferred (ms) {
let cancel, promise = new Promise((resolve, reject) => {
cancel = reject
setTimeout(resolve, ms)
})
return { promise, cancel }
}
click counter example
In this first example, we have a button that counts the user's clicks. The event listener is attached using debounce
, so the counter is only incremented after a specified duration -
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const mycounter = myform.mycounter
// event handler
function clickCounter (event) {
mycounter.value = Number(mycounter.value) + 1
}
// debounced listener
myform.myclicker.addEventListener("click", debounce(clickCounter, 1000))
<form id="myform">
<input name="myclicker" type="button" value="click" />
<output name="mycounter">0</output>
</form>
live query example, "autocomplete"
In this second example, we have a form with a text input. Our search
query is attached using debounce
-
// debounce, deferred
function debounce (task, ms) { let t = { promise: null, cancel: _ => void 0 }; return async (...args) => { try { t.cancel(); t = deferred(ms); await t.promise; await task(...args); } catch (_) { console.log("cleaning up cancelled promise") } } }
function deferred (ms) { let cancel, promise = new Promise((resolve, reject) => { cancel = reject; setTimeout(resolve, ms) }); return { promise, cancel } }
// dom references
const myform = document.forms.myform
const myresult = myform.myresult
// event handler
function search (event) {
myresult.value = `Searching for: ${event.target.value}`
}
// debounced listener
myform.myquery.addEventListener("keypress", debounce(search, 1000))
<form id="myform">
<input name="myquery" placeholder="Enter a query..." />
<output name="myresult"></output>
</form>
Related videos on Youtube
![Startec](https://i.stack.imgur.com/fr49T.png?s=256&g=1)
Startec
Updated on April 06, 2022Comments
-
Startec about 2 years
I am interested in the "debouncing" function in javascript, written here : http://davidwalsh.name/javascript-debounce-function
Unfortunately the code is not explained clearly enough for me to understand. Can anyone help me figure out how it works (I left my comments below). In short I just really do not understand how this works
// 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: The copied code snippet previously had
callNow
in the wrong spot.-
Ry- about 10 yearsIf you call
clearTimeout
with something that isn’t a valid timer ID, it doesn’t do anything. -
Pacerier about 10 years@false, Is that valid standard behavior?
-
Mattias Buelens about 10 years@Pacerier Yes, it is in the spec: "If handle does not identify an entry in the list of active timers of the
WindowTimers
object on which the method was invoked, the method does nothing." -
snegi over 3 yearsThis blog is very helpful loopinfinito
-
-
Startec about 10 years"Note that this overwrites the value of timeout and this value persists over multiple function calls" Isn't timeout local to each debounce call? It is declared with var. How is it overwritten each time? Also, why check for
!timeout
at the end? Why doesn't it always exist (because it is set tosetTimeout(function() etc.)
-
Startec about 10 yearsfor the
immediate && timeout
check. Won't there always be atimeout
(becausetimeout
is called earlier). Also, what good doesclearTimeout(timeout)
do, when it is declared (making it undefined) and cleared, earlier -
Mattias Buelens about 10 years@Startec It is local to each call of
debounce
, yes, but it is shared among calls to the returned function (which is the function you're going to use). For example, ing = debounce(f, 100)
, the value oftimeout
persists over multiple calls tog
. The!timeout
check at the end is a mistake I believe, and it is not in the current underscore.js code. -
Malk about 10 yearsThe
immediate && !timeout
check is for when debounce is configured with theimmediate
flag. This will execute the function immediately but impose await
timeout before if can be executed again. So the!timeout
part is basically saying 'sorry bub, this was already executed within the defined window`...remember the setTimeout function will clear it, allowing the next call to execute. -
Startec about 10 yearsWhy does timeout have to be set to null inside of the
setTimeout
function? Also, I have tried this code, for me, passing intrue
for immediate just prevents the function from being called at all (rather than being called after a delay). Does this happen for you? -
Startec about 10 yearsWhy does timeout need to be cleared early in the return function (right after it is declared)? Also, it is then set to null inside of the setTimeout function. Isn't this redundant? (First it is cleared, then it is set to
null
. In my tests with the above code, setting immediate to true makes the function not call at all, as you mentioned. Any solution without underscore? -
zeroliu almost 9 yearsI have a similar question about immediate? why does it need to have the immediate param. Setting wait to 0 should have the same effect, right? And as @Startec mentioned, this behavior is pretty weird.
-
Gui Imamura almost 9 yearsI second the question above: why would/should one ever set
immediate
totrue
? Isn't it easier to just callfunc
instead of debouncing it? -
Malk almost 9 yearsIf you just call the function then you cannot impose a wait timer before that function can be called again. Think of a game where the user mashes the fire key. You want that fire to trigger immediately, but not fire again for another X milliseconds no matter how fast the user mashes the button.
-
Malk almost 9 yearsEveryone was correct in pointing out the flaw. I had assumed the script in the OP was correct and just needed some explaining. I updated the answer.
-
geoidesic over 6 yearsI don't understand this. If I run
debounce(somfunction, 0.5, false);
nothing happens. What's the trick? -
geoidesic over 6 yearsOh I see... it's for defining functions, not for calling them.
-
CubicleSoft about 5 years"Renaming" is absolutely necessary. The meaning of
this
andarguments
changes inside the setTimeout() callback function. You have to keep a copy elsewhere or that info is lost. -
Usman almost 5 years@malk what was the flaw?
-
Sandy B over 3 yearsUnfortunately I am not able to make any sense from the part args = arguments. Where do the arguments coming from? I think you need to set it as the argument of the return function
-
DollarAkshay about 3 yearsI still cant wrap my head around how timeout is shared across all function calls
-
Mulan over 2 yearsnah typescript is what you get when amateurs design a type system. programming and tech is riddled w examples of bad things that are popular for all the wrong reasons. don’t mistake popular for good.
-
K.Kaur over 2 yearsBest explanation. Thanks