Debounce Deep Dive — JavaScript ES6

tcase360

Tay

Posted on March 26, 2019

Debounce Deep Dive — JavaScript ES6

Lately I’ve been asked a lot about how I would implement a debounce function as an exercise, and I wondered why this question has become prevalent in the front-end engineering world.

The more I was asked this question, the more I thought about why it was asked, and the reasoning I came up with makes sense:

  • It tests your deeper knowledge of JavaScript
  • There is a practical, real-world application
  • These are commonly used in modern front-end development

When looking around, there wasn’t a wealth of information on the inner workings of a debounce function and that was surprising — I know that Underscore has implementations for both debounce and throttle, but I do believe that it’s important to understand on a deeper level what they are doing before using them extensively. This blog’s purpose is to explain the nuances of JavaScript inside this (albeit, simple) implementation. There are a lot of opinions on the “correct” way to implement these functions, and this blog post is not about that. So without further ado, let’s dive right in.

Purpose of Debounce

This function is built in order to limit the amount of times a function is called — scroll events, mousemove events, and keypress events are all great examples of events that we might want to capture, but can be quite taxing if we capture them every single time they fire. In order to combat this, we implement debounce and throttle functions. We won’t discuss the throttle function in this post, but a debounce function will wait until the last time the function is called and then fire after a predetermined amount of time or once the event firing becomes inactive.


Implementation

Let’s take a look at a debounce function implementation in ES6.

const debounce = (fn, time) => {
  let timeout;

  return function() {
    const functionCall = () => fn.apply(this, arguments);

    clearTimeout(timeout);
    timeout = setTimeout(functionCall, time);
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's take a look at this step by step:

  1. Create a wrapper function with two arguments: a callback and an integer for the timeout — this will hold the state of the timeout. Note that the wrapper function will only be called once, when the wrapper function is referenced.
  2. Declare the timeout variable, which will be undefined until the timeout is set in the returned function.
  3. Return a function — this will be called every time the function is called. Make sure that the function returned is not an arrow function, as you will lose context.
  4. Apply this context to callback function, and attach arguments.
  5. clearTimeout if timeout exists.
  6. setTimeout and pass the applied function.

This way, the clearTimeout resets the timeout each time the function is called, and if the function is not called within the time provided, then it will finally fire the function.

Using the function would look like this:

window.addEventListener('keyup', debounce((e) => {
  console.log(e);
}, 1000));
Enter fullscreen mode Exit fullscreen mode

The first argument being passed is the event handler, and the second is the amount of time in milliseconds that we would consider an element “inactive” after the last event is fired.

Explanation

There are a couple parts of this function that can be used as learning points when it comes to JavaScript:

  • The returned function will take the arguments that the event handler should get — even if they aren’t explicitly declared in the function declaration. Just use the arguments variable that is automatically created when inside a function.
  • fn.apply is very handy, and is perfect for this situation as we won’t always know how many arguments are being provided, therefore we can send the full object through. This will also persist the context of our function.
  • The functionCall variable must be declared inside the returned function so we can call it with the correct arguments.
  • We must declare the timeout variable, because if we don't pass a variable into clearTimeout, then it will globally clear timeouts, and we wouldn’t want to interfere in the global scope so as to avoid unwanted side-effects.

Conclusion

This is a problem with a simple-looking solution spanning 11 lines, but it covers a lot of different concepts that can show a deeper understanding of JavaScript if done correctly, like persisting this, returning a function, and the .apply() method, all encapsulated inside a practical problem that can be used in the real world.

💖 💪 🙅 🚩
tcase360
Tay

Posted on March 26, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Memoized getters in ES6 classes
javascript Memoized getters in ES6 classes

April 23, 2024

ES6 Features
javascript ES6 Features

May 6, 2021

Debounce Deep Dive — JavaScript ES6
javascript Debounce Deep Dive — JavaScript ES6

March 26, 2019