Do you need to handle a specific event, but you want to control how many times your handler will be called in a given period of time? This is a very common problem and often happens when listening to events such as scroll, mousemove or resize, which are triggered dozens of times a second. But sometimes it’s desirable even for less “frenetic” events, like, for example, to limit an action that occurs when a certain button is clicked.
In order to understand both patterns, we will use a simple example application that shows the current mouse coordinates in the screen and how many times these coordinates were updated. By looking at this counter, you will find it easier to compare how these two techniques can control the calling of an event handler. This is our app without any event control technique applied:
render();
document.body.addEventListener('mousemove', logMousePosition);
let callsCount = 0;
const positionsEl = document.querySelector('.app__positions');
function logMousePosition(e) {
callsCount++;
positionsEl.innerHTML = `
X: ${e.clientX},
Y: ${e.clientY},
calls: ${callsCount}
`;
}
function render() {
document.querySelector('#app').innerHTML = `
# Mouse position (Without event control)
`;
}
And here is how this initial version of our app behaves:
Did you notice the calls count? In just 7 seconds, the logMousePosition function was called 320 times! Sometimes you don’t want a given event handler to be called so many times in a period of time so short. Now, let’s see how to solve this issue.
The throttle pattern limits the maximum number of times a given event handler can be called over time. It lets the handler be called periodically, at specified intervals, ignoring every call that occurs before this wait period is over. This technique is commonly used to control scrolling, resizing and mouse-related events.
We need to adapt the example app to use the throttle pattern. In order to do so, let’s change the addEventListener call of the original code to include the throttling logic:
let enableCall = true;
document.body.addEventListener('mousemove', e => {
if (!enableCall) return;
enableCall = false;
logMousePosition(e);
setTimeout(() => enableCall = true, 300);
});
In the above code:
Now, let’s see how the app behaves after applying this technique:
The throttling logic works perfectly! Now, there’s a minimum interval of 300 milliseconds between the updates, drastically reducing our calls count.
In order to make this code reusable, let’s extract it to a separate function:
document.body.addEventListener('mousemove', throttle(logMousePosition, 300));
function throttle(callback, interval) {
let enableCall = true;
return function(...args) {
if (!enableCall) return;
enableCall = false;
callback.apply(this, args);
setTimeout(() => enableCall = true, interval);
}
}
The logic here is the same as above. When called, the throttle function returns a new event listener. In this version, we’re using a common function in order to preserve the original this context and apply it to the passed callback.
The debounce pattern allows you to control events being triggered successively and, if the interval between two sequential occurrences is less than a certain amount of time (e.g. one second), it completely ignores the first one. This process is repeated until it finds a pause greater than or equal to the desired minimum interval. Once it happens, only the last occurrence of the event before the pause will be considered, ignoring all the previous ones. A common use case for this pattern is to limit requests in a search box component that suggests topics while the user is typing.
Let’s adapt our example app to use the debounce pattern:
let debounceTimeoutId;
document.body.addEventListener('mousemove', (e) => {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(() => logMousePosition(e), 300);
});
The above piece of code contains the essence of the debounce pattern. Every time the event we want to control is fired, we schedule a call to the handler for 300 milliseconds later (by using setTimeout) and cancel the previous scheduling (with clearTimeout). This way, we’re constantly delaying the execution of the handler function until the interval between two sequential events is equal to or greater than a given threshold time.
To sum up:
Let’s say we have a given event being triggered two times (A and B) and there’s an X interval between these two occurrences. If we want to control this event by applying a debounce of Y milliseconds, here’s how it will work:
Now, let’s see how it behaves in practice:
Finally, we can extract the pattern logic to a reusable function. This way, we can easily apply it in similar situations in the future:
document.body.addEventListener('mousemove', debounce(logMousePosition, 300));
function debounce(callback, interval) {
let debounceTimeoutId;
return function(...args) {
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(() => callback.apply(this, args), interval);
};
}
#javascript #design-pattern