FrontendPro

26Implement debounce() with cancel() and flush()

mediumAbout 35 minutesCodingFreetimersclosuresperformance

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 fn with the arguments and this of the latest call.
  • The debounced function returns the result of the last completed call (undefined before the first).
  • After fn has run, the debounced function can be used again.

Try it yourself

Write your solution, run it, then submit to check it against the tests.

solution.jsJavaScript

Press Run or ⌘ Enter to execute your code.

Video walkthroughs

Stuck? Watch someone solve it, then try again on your own.

Debouncing in Javascript | Flipkart UI Interview QuestionAkshay Saini on YouTubeOpen on YouTube
Javascript Interview Questions ( Debouncing and Throttling ) - Implementation, Examples etcRoadsideCoder on YouTubeOpen on YouTube

Step by step

Build the solution one idea at a time.

  1. 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. 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, so fn runs once, wait ms after the final call.

  3. 3

    Use a regular function for the wrapper

    this must come from how the debounced function is called, for example as obj.save(). An arrow function would freeze this to wherever debounce was called.

  4. 4

    Put the real call in one place

    invoke() clears the timer id, calls fn.apply(savedThis, savedArgs), clears the saved arguments and stores the result. Both the timer and flush use it, so they always behave the same.

  5. 5

    Implement cancel and flush

    cancel clears the timer and forgets the saved arguments. flush does nothing but return the last result when no call is pending; otherwise it clears the timer and calls invoke() 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 calling fn.
  • After flush(), the old timer cannot fire a second call.
  • wait = 0 still defers the call to a later task.

Follow-up questions

Answer each one out loud first, then reveal the model answer.