A couple of years ago, I was asked to write my own debouncer function at an interview. Unfortunately, I didn't have enough time to complete this question, so, I wanted to spend sometime to solve this problem on my own.
So, what is a debounced function?
A debounced function is a delayed function that can only be invoked if a specified amount of time has passed between successful calls. This is often useful when you want to prevent the number of API calls that are made when implementing something like an autocomplete field.
What is a debouncer function?
A debouncer function is a function that takes two arguments -
- a callback (i.e. the function you want to debounce)
- an amount of time in milliseconds between successful calls
and returns a new "debounced" version of the function passed in as the first argument.
The Code
As aforementioned, we want to return a new function from the debouncer:
function debounce(callback, timeout) {
return (...args) => {
// ...
}
}Since the new function can contain any number of arguments, we will collect all arguments in an array by using the ES6 spread syntax which we will call args.
Now, we want to "delay" the function call of callback by timeout milliseconds. This part is easy, since we can use setTimeout to simulate a delayed function call.
function debounce(callback, timeout) {
return (...args) => {
setTimeout(() => {
callback.apply(null, args)
}, timeout)
}
}We are getting close but, you will immediately notice a bug if you try to use our debouncer function in its current state.
<button id="debounced-button">click me</button>const debouncedHandler = debounce(() => console.log('Clicked!'), 1000)
const button = document.getElementById('debounced-button')
button.addEventListener('click', debouncedHandler)
Everytime we click the button, we create a new setTimeout that will invoke the callback as soon as timeout milliseconds has passed. Therefore over time, our callback will be invoked as many times as we clicked it. This is not the intended behavior. We need to figure out a way to "cancel" any previous setTimeouts, so, we only successfully invoke callback after the delay has entirely elapsed without interruption. Canceling a setTimeout is easy using the clearTimeout function that will clear a setTimeout so long as you pass the unique ID returned from the setTimeout. But how do we save the previous setTimeout? I think this is the real challenge of this interview question. It is actually quite simple! We just need to make use of function closures.
function debounce(callback, timeout) {
let timeoutRef
let value
return (...args) => {
clearTimeout(timeoutRef)
timeoutRef = setTimeout(() => {
value = callback.apply(null, args)
}, timeout)
return value
}
}Let's break this code down further.
- We declare two new variables
timeoutRefandvalueoutside of the debounced function to hold the ID of thesetTimeout, and thecallbackreturn value, respectively. - We clear any stale
setTimeoutby callingclearTimeoutwith the ID associated with the previoussetTimeout. You might be wondering - "But what iftimeoutRefisundefined?" For example, on the first run?clearTimeoutwill not do anything in that case and will move on to the next line of code. - We save the ID returned from the new
setTimeoutinto thetimeoutRefvariable. - finally we save the return value of the
callbackfunction invoked withargs, so, that we can return this value from the debounced function (I am using theapplymethod on theFunctionprototype, so, that we can invokecallbackwith theargsarray. You can read more aboutapplyhere.)
And there you have it, a custom debouncer function! This is definitely a great interview question to test the candidates understanding of function closures. I hope this helps you as well in one of your future interviews!
