Implement debounce in JavaScript

Maayan Savir
6 min readOct 2, 2020

Debounce function limits the rate at which a function can fire.

Photo by EVGENIY KONEV on Unsplash

We want to use debounce every time we trigger an event/make an API call/run a process that is a heavy task.

For example, an autocomplete. When a user types on an input element we want to send a server request to fetch the data that is relevant to the value the user typed. Imagine we will fire an API call every time a user is typing. Perform an API call is a heavy task so we really don’t want to perform it every time a key was pressed.

What we do want to do?
We want to limit the requests by sending the server more data at once instead less data with more requests.
We want to keep the value of the user input and only when they stop typing we will perform an API call and send the data.

Debounce function limits the rate at which a function can fire.

Side note: debounce and throttle are two similar functions but different. While debounce function limits the rate at which a function can be fire (for example — “execute this function only if 100 milliseconds have passed without it being called.”) throttle enforces a maximum number of times a function can be called over time (for example, “execute this function at most once every 100 milliseconds.”)
Check out this demo that illustrates the difference between debounce and throttle.

Implement a debounce function

Returns a function, that, as long as it continues to be invoked, will not be triggered. The function will be called after it stops being called for X milliseconds.

To get started with the implementation, we need to be familiar with two functions — setTimeout() and clearTimeout() .
setTimeout() — The setTimeout() method sets a timer which executes a function or specified piece of code once the timer expires.
clearTimeout() — The clearTimeout() cancels a timeout previously established by calling setTimeout().

Take a look at the console logs in the codepen example above. setTimeout() returns an id which we can cancel a timeout using clearTimeout() by passing that id.

clearTimeout() cancels the previously operation established by setTimeout() .

With that in mind, let’s write some code

The gist above does what we wanted it to do.
We declared a timeoutvariable and assigned the myDebounce function when there is a scroll event on the window object.
myDebounce function first, clears the timeout variable with clearTimeout()and then set it to a new id number using setTimeout() .

It is important to first clear the timeout variable and then assign a new one so we keep tracking the same timeout id

Every time we scroll the window, we are clearing the previously created timeout ID and assign a new one. The minute we stop scrolling, new and last timeout will be created (and will not get cleared) and we will see expensive operation on the console after 300 milliseconds.

However, this is a BAD code

what? why?
While the code snippet above does the job, it is a bad written code and not recommended.

When we declare a variable in the global scope the variable is accessible from anywhere in the application, meaning we have the ability to use this variable everywhere and most importantly we can assign it a new value from everywhere in our app.
To overcome this issue, we need to enclose this value in its own scope. Whenever we declare a function, we are creating a new scope in the app. Each scope has access to its own variables/functions and manage its own state.

In JavaScript, we have a nesting scope that works one way. Children have access to their parent scope but it is not the other way, parents don’t have access to their children scope.

To have the timeout variable persists in its own scope, we simply need to create it inside a function. Now before we go and do something like that

const myDebounce = () => { let timeout; clearTimeout(timeout); timeout = setTimeout(() => { console.log(“expensive operation”)},   300);};window.addEventListener(‘scroll’, myDebounce);

let’s understand why this will not work.
If we would declare the variable inside the function, every time we scroll the function will get triggered and create a new timeout variable set to nothing (undefined), and since when we pass an invalid timer ID to clearTimeout(), it silently does nothing and clearTimeout(timeout); becomes useless and we lose the debounce functionality.

What we want to do, is to create the timeout variable inside the myDebounce function, but have the function to return an anonymous function.

We just created a closure to handle the private variables our function is holding.

Wait, how come timeout is being declared only once if we use myDebounce() every time we scroll?
that is a great question! So while it might seems like we call myDebounce() every time we scroll , it is not exactly the case. When the script is loading, it gets to line 11window.addEventListener(‘scroll’, myDebounce()) and initialize myDebounce() function for the first and only time. The function returns an anonymous function which will be the one that will get triggered every time we will have a scroll event. myDebounce() eventually becomes a reference to the anonymous function.

Let’s continue and clean our function a bit. We want the operation setTimeout() executes to be dynamically passed into myDebounce() and the delay time.

Ok, so we have a nice looking working function implementing debounce functionality.
But hey, it is still a BAD code.
What? what now?

Remember when we were talking about the scope? so now, by looking at the code, we have 3 different scopes and because of it, each function has its own context and therefore its own reference to the this keyword.

1.myDebounce scope — refers to the window object since it is executed globally
2.anonymous function scope-could be window or object - depends how you call myDebounce. In out example, it refers to the window element (since we trigger the event by scrolling the window)
3.setTimeout scope- refers to the window object

Let’s see an example!

we can assume that we have a button html element with an id debounceButton

This is the console after running the code and clicking the button once.

on line 6, myDebounce function context, this keyword, refers to the window.
on line 10, the anonymous function context, this keyword, refers to the button (the object that triggers the function).
on line 6, setTimeout context, this keyword, refers to the window.

Another issue is, what will happen if we want to pass N parameters to myDebounce function?
as we discussed earlier, the anonymous function is what being executed every time we call myDebounce . So while the anonymous function will get the arguments, eventually, we will want func() to get them.
those can be retrieved in the anonymous function via a reserved keyword arguments.

We need to bind the anonymous function context into the setTimeout context so when calling the anonymous function the context and arguments would be the same

The apply() method allows you to call a function with a given this value and arguments provided as an array.
when using func.apply(context,args) you basically tells the func() method to use the this as the context argument, and the args as the arguments.

The reason we would want to use apply() (and not call() for example) is because call() accepts an argument list, while apply() accepts a single array of arguments

Conclusion

debounce function is very small, you can change it to your needs but the basic functionality is only about 6 lines of code but still covers a lot of interesting concepts in JavaScript like scope, closure and the lexical context (this keyword).

--

--