26Implement debounce() with cancel() and flush()
Problem
Implement debounce(fn, wait). It returns a debounced function that delays calling fn until wait milliseconds have passed since the last call. Every new call restarts the timer, so a burst of calls produces a single call to fn with the latest arguments.
The debounced function also has two methods:
cancel(): drop the pending call, if there is one.flush(): run the pending call now and return its result. If nothing is pending, return the result of the last call.
const save = debounce((text) => api.save(text), 500);
input.addEventListener("input", (e) => save(e.target.value));
window.addEventListener("beforeunload", () => save.flush()); // do not lose the last edit
cancelButton.onclick = () => save.cancel();Requirements
- Call
fnwith the arguments andthisof the latest call. - The debounced function returns the result of the last completed call (
undefinedbefore the first). - After
fnhas run, the debounced function can be used again.
Try it yourself
Write your solution, run it, then submit to check it against the tests.
Press Run or ⌘ Enter to execute your code.
Video walkthroughs
Stuck? Watch someone solve it, then try again on your own.
Step by step
Build the solution one idea at a time.
- 1
Keep the state in a closure
You need four variables that outlive each call: the timer id, the latest arguments, the latest
this, and the last result. - 2
Restart the timer on every call
Each call saves its arguments and
this, clears the previous timer and sets a new one. Only the last timer in a burst survives, sofnruns once,waitms after the final call. - 3
Use a regular function for the wrapper
thismust come from how the debounced function is called, for example asobj.save(). An arrow function would freezethisto whereverdebouncewas called. - 4
Put the real call in one place
invoke()clears the timer id, callsfn.apply(savedThis, savedArgs), clears the saved arguments and stores the result. Both the timer andflushuse it, so they always behave the same. - 5
Implement cancel and flush
cancelclears the timer and forgets the saved arguments.flushdoes nothing but return the last result when no call is pending; otherwise it clears the timer and callsinvoke()right away.
Approach
A closure holds one timer and the latest call's arguments. Every call replaces both; when the timer finally fires, fn runs once with the latest arguments. cancel and flush are the two ways to settle the pending call early.
Solution
function debounce(fn, wait = 0) {
let timer = null;
let savedArgs;
let savedThis;
let result;
function invoke() {
const args = savedArgs;
const self = savedThis;
timer = null;
savedArgs = savedThis = undefined;
result = fn.apply(self, args);
return result;
}
function debounced(...args) {
savedArgs = args;
savedThis = this;
clearTimeout(timer);
timer = setTimeout(invoke, wait);
return result;
}
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
savedArgs = savedThis = undefined;
};
debounced.flush = function () {
if (timer === null) return result;
clearTimeout(timer);
return invoke();
};
return debounced;
}Complexity
Time: O(1) per call. Space: O(1): one timer and the latest arguments.
Edge cases
- A burst of calls produces exactly one call to
fn, with the last arguments. cancel()with nothing pending does nothing.flush()with nothing pending returns the last result without callingfn.- After
flush(), the old timer cannot fire a second call. wait = 0still defers the call to a later task.
Follow-up questions
Answer each one out loud first, then reveal the model answer.