A debounce function is a common coding question that prospective interviewees are asked. In this article I will go through in detail how to create a debounce function from scratch. I will also like to focus on understanding how the debounce function works, this is part of the coding question series. First let's understand what the debounce function is and why we need it.
When working to ensure optimum performance you will sometimes need to think about how your application will be used. If you have certain items in the browser that need to have their position recalculated when the user resizes for example, then you will need to ensure that the function that is invoked does not get called too often - as this will slow down the performance of your app.
Or maybe you have a like button next to a picture, and everytime a user clicks on the like button you make an AJAX call to your API - you want to ensure that this API call is made only after a certain time has elapsed as opposed to making a call everytime the user clicks the like button. In these two scenarios, and other situations, you will benefit from using what's called a debounce function.
A debounce function limits the amount of times a function is called. This could be the function that calculates coordinates on window resize, or the function that makes the AJAX call when you click on the like button. Capturing these events and invoking a function call to deal with them for each and every event can be quite taxing as you can imagine.
A debounce function acts as a delay mechanism to prevent unneccesary calls to functions when the user clicks multiple times in quick succession. You will have most likely experienced the debounce function in action when using a search feature in an app. When start typing after a small delay, you get suggestions to autocomplete your search. Now, add the function that handles the search criteria fired on very keydown, then this would impact the relevant dropdown of possible search terms and it would unnecessarily call an API multiple times. This would also impact on the app performance.
Now we understand what the debounce function is and why we need it, we will implement a couple of variations of the debounce function. We'll start with the classic use case where we need to simply delay a function being invoked for a limited time to prevent unnecesary function calls when the user clicks in quick succession.
debounce
function that takes a required callback
function, a delay
in millisecondsthis
.delay
has elapsed. So this means that if the user were to click five times, the callback function would only be called once and only after the delay
time has elapsed.debounce
function to take in a third optional boolean parameter - immediate
that is initially set to false
. When this parameter is true
, then the first call to the debounced function should immediately invoke the callback function. Also, the callback function should not be able to execute again until the delay
milliseconds have elapsed.Now before we look at the solution let's look at some things that we'll need to solve this problem. From looking at outline of problem above, we can deduce a few things that we'll need:
setTimeout
function JavaScriptthis
, so we'll need to make use of native functions like call
, apply
and bind
One thing that is important to understand when solving coding problems is to understand the problem fully, and try to deduce as much detail as you can. If this means that you need to ask clarifying questions, then you should ask as many questions as possible to make sure that you fully understand the problem at hand.
Below is the implementation of the debounce function that you can use to limit the number of times a function is called. So, if we take the example where the user clicks the like button, we only want to make the AJAX call after say pause of two seconds. We can achieve this with the following debounced function implementation:
function debounce(fn, time) {
let timeoutID
return function () {
const args = arguments
clearTimeout(timeoutID)
timeoutID = setTimeout(() => {
fn.apply(this, args)
}, time)
}
}
As a solution it doesn't actually involve much code. This solution has all of the things we deduced previously, it is a Higher Order Function, so it returns a new transformed function. It also passes all the callbacks that are given when the debounced function is called. Finally, it also sets the context of the this
keyword correctly.
Right, let's now look at the solution step by step, starting with the timeoutID
variable:
function debounce(fn, time) {
let timeoutID;
...
The timeoutID
variable here will hold an ID of a call to setTimeout()
function. The setTimeout()
function when called returns a positive integer value which identifies the timer created by the call to setTimeout()
. This ID integer can be cleared by passing it to the clearTimeout()
function. The global clearTimeout()
function cancels a timeout previously set by the setTimeout()
method.
For now we are not actually going to assign it anything, and at the beginning it will simply be undefined
. The next part is where we return a new function:
function debounce(fn, time) {
let timeoutID;
return function () {
const args = arguments
clearTimeout(timeoutID)
...
}
}
The new function that we return will do a few things, first it will save the array like arguments
value, that is available inside the body of every function and contains all of the arguments supplied, to a variable args
. We will use this variable args
later when invoking the given callback function fn
and use it to pass along any parameters that are given to the returned callback function fn
when it's invoked.
Next we use the clearTimeout()
function to cancel any timeout that was previously set by a call to setTimeout()
. This is an important step because we need to cancel any timeouts that are currently active, so this means that if the user clicked twice in succession. The first click will have registered a call to setTimeout()
, now unless we cancel this timeout the callback given to setTimeout()
will be executed and it will mean that our callback fn
will be called twice, which we don't want.
So in other words, if the user clicks multiple times, all of the timeouts are cancelled and overwritten with a new timeout, that one being the last click. Also, here we are making use of a closure - the timeoutID
variable is declared outside the anonymous function that is returned. In closures, the returned function will have access to the outer scope variables.
Had we declared the variable inside the anonymous function then this will mean each time the debounce function is called, the returned anonymous function would create a new instance of timeoutID
and we would not be able to clear any lingering timeouts when debounce function is called before delay
milliseconds has elapsed. let's continue to the next part of the anonymous function:
function debounce(fn, time) {
let timeoutID
return function () {
const args = arguments
clearTimeout(timeoutID)
timeoutID = setTimeout(() => {
fn.apply(this, args)
}, time)
}
}
Once we clear any outstanding timeouts using the clearTimeout(timeoutID)
expression, we overwrite the timeoutID
with a new timeout ID. So, this means that if we keep clicking to invoke the debounced function, all timeouts that are created will be cleared and only the last call to the callback will result in a new timeout ID that overwrites any previous on timeout ID.
Then inside the callback to the setTimeout()
function we call the supplied callback fn
using the apply
method and set the context for this
to be the same as the anonymous function and then we pass along any arguments that the returned anonymous function is called with using the variable args
.
There isn't much code, but there's quite a few things happening here and it's important that you understand how the debounce function works, instead of just memorizing the function signature, as you could get asked questions about your implementation. Again, to solve this problem you will need to have an understanding of:
setTimeout()
and clearTimeout
methodsthis
will be different depending on how the function is calledapply
methodUnless you have a good understanding of these topics it will be difficult to fully understand how the debounce
function works under the hood. Next, let's implement the feature where we can support an optional third parameter immediate
:
function debounce(fn, delay, immediate = false) {
let timeoutID
return function (...args) {
clearTimeout(timeoutID)
if (immediate && !timeoutID) {
fn.apply(this, args)
}
timeoutID = setTimeout(() => {
if (!immediate) {
fn.apply(this, args)
}
timeoutID = null
}, delay)
}
}
So for this new requirement I've made some slight changes, now when we clear any lingering timeout IDs, we do a check on the immediate
variable. If this is true and timeout ID is a falsy value we call the callback fn
immediately setting the this
context and passing along any arguments.
Now when the function is called immediately, we don't want to allow the callback fn
to be called again until after the specifed delay
milliseconds. We again have our timeoutID
to be overwritten with the new timeout, and if we want to invoke again immediately it has to wait for delay
milliseconds to be called again. We also want a way to set timeoutID
to a falsy value, so inside the callback to setTimeout()
we do a check in the immediate
variable, if this is not true we simply call the callback function. In any case we set the timeoutID
to null so that we are able to call the fn
callback immediately if we need to. With this, we meet all of the requirements set out previously.
The debounce function is a common coding interview question, and it essentially tests your understanding of performance requirements, the JavaScript language and problem solving skills. As I mentioned alread you will need to be familiar with some advanced JavaScript concepts such as functions closures and higher order functions. I hope that this article has been able to explain in detail how the debounce function works, and if you don't understand don't worry - just make sure you understand closures, setTimeout
, clearTimeout
, function arguments and this
properly and come back to it.