back to series

Implement a debounce function

Debouncing is a technique used to control how many times we allow a function to be executed over time.

When a JavaScript function is debounced with a wait time of X milliseconds, it must wait until after X milliseconds have elapsed since the debounced function was last called.

From my personal experience, the implementation of debouncing with following variations are usally asked:

Variation: Basic implementation

1. Basic debounce implementation

./src/debounce.js
export default function debounce(func, wait) {
let timerId = null;
function debouncedFnImpl(...args) {
const thisArg = this;
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => {
timerId = null;
func.call(thisArg, ...args);
}, wait);
}
return debouncedFnImpl;
}
// Usage of debounce function
const debouncedFn = debounce(() => {
console.log('I am called');
}, 1000);
const input = document.getElementById('input');
input.addEventListener('change', debouncedFn);
Variation: With Immediate Flag

2. Variation with immediate flag

Immediate flag denotes that the function for the first time must be invoked immediately without any delay. For further invocations, it follows same rules as mentioned above (basic implementation).

./src/debounce.js
export default function debounce(func, wait, immediate) {
let timerId = null;
let hasBeenCalledImmediately = false;
function debouncedFnImpl(...args) {
const thisArg = this;
if (timerId) {
clearTimeout(timerId);
}
// Check for the `immediate` flag and invoke immediately
if (immediate) {
hasBeenCalledImmediately = true;
func.call(thisArg, ...args);
}
timerId = setTimeout(() => {
timerId = null;
func.call(thisArg, ...args);
}, wait);
}
return debouncedFnImpl;
}
// Usage with immediate flag
const debouncedFn = debounce(
(e) => {
console.log(e.target.value);
},
1000,
true
);
const input = document.getElementById('input');
input.addEventListener('change', debouncedFn);
Variation: With Leading Flag

3. Variation with leading flag

Leading flag denotes that the function must be called on the leading edge, i.e, invoke immediately (like immediate flag), but not just for the first time. In basic implementation we are doing a trailing edge debounce of the function.

./src/debounce.js
export default function debounce(func, wait, leading) {
let timerId = null;
function debouncedFnImpl(...args) {
const thisArg = this;
if (timerId) {
clearTimeout(timerId);
}
const canCallNow = leading && !timerId;
// Check for the `canCallNow` flag and invoke immediately
if (canCallNow) {
func.call(thisArg, ...args);
}
timerId = setTimeout(() => {
timerId = null;
if (!leading) {
func.call(thisArg, ...args);
}
}, wait);
}
return debouncedFnImpl;
}
// Usage with leading flag
const debouncedFn = debounce(
(e) => {
console.log(e.target.value);
},
1000,
true
);
const input = document.getElementById('input');
input.addEventListener('change', debouncedFn);
Variation: With Flush and Cancel Methods

4. Variation with flush and cancel methods

This variation has few add-ons on the basic implementation and typically involves addition of two methods:

  1. cancel(): method to cancel pending invocations.
  2. flush(): method to immediately invoke any delayed invocations.
./src/debounce.js
export default function debounce(func, wait) {
let timerId = null;
let thisArg = undefined;
let invocationArgs = undefined;
function cancel() {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
function invoke() {
if (timerId) {
func.call(thisArg, ...invocationArgs);
timerId = null;
}
}
function debouncedFnImpl(...args) {
thisArg = this;
invocationArgs = args;
// we first cancel any pending invocations
cancel();
// we then create delayed execution of the function (trailing edge invocation)
timerId = setTimeout(() => {
invoke();
}, wait);
}
// attach cancel and flush utility methods
debouncedFnImpl.cancel = cancel;
debouncedFnImpl.flush = invoke;
return debouncedFnImpl;
}
// Usage with leading flag
const debouncedFn = debounce(
(e) => {
console.log(e.target.value);
},
1000,
true
);
const input = document.getElementById('input');
input.addEventListener('change', debouncedFn);
// Using the cancel method : it cancels any delayed invocations
debouncedFn.cancel();
// Using the flush method: any pending invocation if present is called immediately
debouncedFn.flush();

Further Reading

I strongly encourage you to explore and tackle additional questions in my Closure Questions for Frontend Interviews blog series.

By doing so, you can enhance your understanding and proficiency with closures, preparing you to excel in your upcoming frontend interviews.

Wishing you best. Happy Interviewing 🫡.