SI

[ Writing Your Own Debouncer Function ]

March 22, 2019

🕰 4 min read

Last year, I landed an onsite interview at a company in SF and was grilled by six senior engineers for five hours. One of the last technical questions they asked me was to “write a debouncer function.” It seemed simple enough - I need to write a function that takes two arguments:

  1. a callback function and
  2. timeout in milliseconds

and returns a new function that is debounced by the amount specified by timeout i.e. the returned function can only invoke the callback function after (at least) the specified amount of time has passed inbetween successful calls. But since I took longer on the other parts of the interview and was completely drained, I didn’t have enough time or energy to make a dent in the problem. Unfortunately (or fortunately,) I didn’t get the job (which was totally fine because everything about that company didn’t feel right.) Anyway, I left the interview with this question lingering in my mind: “write a debouncer function.” I had some ideas but never got around to implementing it until now 😅. So here we go:

My Thought Process…

As aforementioned, we want to write a function called debouncer that returns a “debounced” function that can only invoke the callback function passed into it only after (at least) the time specified (timeout) has passed inbetween successful calls.

function debouncer(callback, timeout) {
  // returns a debounced version of callback
  return (...args) => {
    // TODO: invoke `callback` only after `timeout` milliseconds have passed inbetween calls to callback;
  }
}

Then we would use the debouncer function like this:

const callback = () => alert('INVOKED! 🎉')
const debounced = debouncer(callback, 3000) // wait 3 second inbetween calls to callback;

const buttonNode = document.querySelector('button')
buttonNode.addEventListener('click', debounced)

I can now click on this button as many times as I want, however, callback can only be invoked after at least 3000 milliseconds (or 3 second) has passed inbetween successful calls.

demo of the debouncer in action.

demo of the debouncer in action

Great! So how do we implement this logic to “debounce” calls to callback unless timeout milliseconds has passed inbetween calls? The key is using our friend: function closures! We will keep a variable that we will toggle to determine if the callback can be invoked. Let’s call this variable OK. Hence we would have something like:

function debouncer(callback, timeout) {
  // returns a debounced version of callback
  let OK = true
  return (...args) => {
    if (OK) {
      // 1. TODO: invoke `callback` with `args` and capture any value it might return
      // 2. TODO: set `OK` to false
      // 3. TODO: set an interval to wait `timeout` seconds to set `OK` back to true
      // 4. TODO: return `callback` response
    }
  }
}

Let’s tackle these todos!

1. Invoke callback with args and capture any value it might return

Since we are invoking callback with an array of args, we want to make use of the apply method on the Function prototype:

// ...

// capture the returned value from the invocation of `callback` with `args`.
const value = callback.apply(null, args) // null is just a placement holder for the context in which `callback` is being applied.

// ...

2. Set OK to false

This step is pretty self explanatory.

3. Set an interval to wait timeout seconds to set OK back to true

// ...
setInterval(() => {
  OK = true
}, timeout)
// ...

4. return callback response

// ...
return value
// ...

Putting it all together we would have something like this:

function debouncer(callback, timeout) {
  let OK = true
  return (...args) => {
    if (OK) {
      const value = callback.apply(null, args)
      OK = false
      setTimeout(() => {
        OK = true
      }, timeout)
      return value
    }
  }
}

And there you have it: a simple debouncer function to “debounce” any callback function you pass to it. This was definitely a great interview question to test the interviewee on their knowledge of function closures. I am still not sure if this is the best way to implement it since it doesn’t ever clear the setTimeout in case the button is removed from the DOM???? If you have any comments or suggestions please feel free to DM on Twitter (link to my Twitter is down below.) Thanks for the read!


a not so professional head shot.

Scott Iwako

👋 Hello, thanks for the read! If you found my work helpful, have constructive feedback, or just want to say hello, connect with me on social media. Thanks in advance!