How to create a debounce function

How to create a debounce function

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.

What is the debounce function and why do 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.

Implementation of the debounce function

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.

Outline of problem

  1. Create a debounce function that takes a required callback function, a delay in milliseconds
  2. Calling the debounced function should return a new transformed debounced version of the supplied callback function, that takes in the same parameters as the original callback function and should has have the correct context for this.
  3. Repeatedly calling the debounced function in quick succession, should not call the callback function each time. Rather the callback function should only be called when the specified 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.
  4. Enhance your 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.

Understanding the problem

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:

  • A Higher order function - possibly making use of function closures
  • Make use of setTimeout function JavaScript
  • We'll need to bind the context for this, 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.

Solution step by step

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:

  • Closures
  • setTimeout() and clearTimeout methods
  • Understand that this will be different depending on how the function is called
  • How to invoke a function using the apply method

Unless 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.

Conclusion

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.