We all know Davidwalsh's debounce function. The post is from 2014 but even now many developers use this and most of the Youtube tutorials are based on this.
If you are not familiar with Davidwalsh debounce function take a look at it here:
https://davidwalsh.name/javascript-debounce-function
When I looked at this, it didn't strike me. I have to bang my head many times to understand why he ended up writing the way it is.
So I ended up refactoring the code using a new ES6 arrow function.
Our brains are not made the same way, so some people may get my function better and others don't. Important thing is, you understand what you are writing and your team agrees.
That being said. here we go
const debounce = (func, delay) => {
let timerId;
return () => {
clearTimeout(timerId);
timerId = setTimeout(func, delay); // start the timer
};
};
That's it??!! Yes! this is the bare minimum version of the debounce. It utilize the closures to store the timerId
in the parent scope.
You can try it on the sandbox here: https://codesandbox.io/s/es6-debounce-example-llgu7?file=/src/index.js
Davidwalsh's debounce function has more features built in.
Now, instead of adding all features to make the function complex, let's isolate the features, so we can better understand how each feature effects the function. At the end we combine both features into single function.
- pass arguments to the function
- execute function immediately then delay
Argument Version
const debounce = (func, delay) => {
let timerId; // keep track of current timer
// return the function
return (...args) => {
const boundFunc = func.bind(this, ...args);
clearTimeout(timerId);
timerId = setTimeout(boundFunc, delay); // start the timer
};
};
This was easy to add. We just needed to bind the arguments to the function.
https://codesandbox.io/s/es6-debounce-arguments-example-2p4bp?file=/src/index.js
Immediate Version
const debounce = (func, delay) => {
let timerId;
return () => {
if (!timerId) {
func();
}
clearTimeout(timerId);
timerId = setTimeout(() => {
timerId = null;
}, delay);
};
};
As you can see, on the initial call, we execute the function immediately then set the timerId with the callback function that will null the timerId after the delay.
Here is the immediate version of the sandbox:
https://codesandbox.io/s/es6-debounce-immediate-example-737vm?file=/src/index.js
Combined all
const debounce = (func, delay, immediate) => {
let timerId;
return (...args) => {
const boundFunc = func.bind(this, ...args);
clearTimeout(timerId);
if (immediate && !timerId) {
boundFunc();
}
const calleeFunc = immediate ? () => { timerId = null } : boundFunc;
timerId = setTimeout(calleeFunc, delay);
}
}
for the bonus, we can change this to throttle as well. Only differences is, timing of resetting the timerId
. For throttle, we don't clearTimeout
, we just null
the timerId
after the execution of the function.
const throttle = (func, delay, immediate) => {
let timerId;
return (...args) => {
const boundFunc = func.bind(this, ...args);
if (timerId) {
return;
}
if (immediate && !timerId) {
boundFunc();
}
timerId = setTimeout(() => {
if(!immediate) {
boundFunc();
}
timerId = null; // reset the timer so next call will be excuted
}, delay);
}
}
https://codesandbox.io/s/es6-throttle-example-2702s?file=/src/index.js
Top comments (5)
Hey Monaye,
your simplified version of the function without the immediate argument is very helpful, thank you, I learned a lot and begin understanding debouncing (slowy).
I like how you refer to David Walsh's original post as some classical legacy.
However, it is not from 2004, but from 2014. :-)
Best wishes,
Martin
Oops.. Thank you :wink
How would you refactor the 'once' function?
I will be using your improved ES6 debounce version from now on ... Thanks!
On a side note
There is a less popular but quite useful function async-debounce from Julian Gruber that
Is there a ES6 approach to that or another way of achieving that same result?
This is a very useful function and I think that as many developers as possible should know about it. Thanks for the update!