3
\$\begingroup\$

The Debounce technique allow us to “group” multiple sequential calls in a single one - The debounce() function forces a function to wait a certain amount of time before running again.

I wrote both debounce_1 & debounce_2 functions while solving a programming challenge, they produce the same result but I want to know which implementation is better and why, as I'm currently studying JavaScript to improve the quality of my code.

/* Write a function called 'debounce' that accepts a function and returns a 
   new function that only allows invocation of the given function after 
   'interval' milliseconds have passed since the last time the returned 
   function ran.

   Additional calls to the returned function within the 'interval' time 
   should not be invoked or queued, but the timer should still get reset. */

function debounce_1(callback, interval) {
  let lastTime = -Infinity;
  
  return function(){
    let now = Date.now(), res;
    
    if (now - lastTime > interval) {
      res = callback();
    }
    
    lastTime = now;
    return res;
  }
}

function debounce_2(callback, interval) {
  let timeout = null;
  
  return function(){
    let res;
    
    if (!timeout) { res = callback(); }
    
    clearInterval(timeout);
    timeout = setTimeout(() => timeout = null, interval);
    
    return res;
  }
}

/* TESTING */
function hello() { return 'hello'; }

// UNCOMMENT BELOW TO TRY EITHER ONE:
const sayHello = debounce_1(hello, 3000);
// const sayHello = debounce_2(hello, 3000);

console.log( sayHello() );                                 // -> 'hello'
setTimeout(function() { console.log(sayHello()); }, 2000); // -> undefined
setTimeout(function() { console.log(sayHello()); }, 4000); // -> undefined
setTimeout(function() { console.log(sayHello()); }, 8000); // -> 'hello'

\$\endgroup\$
1
  • \$\begingroup\$ One (very minor) advantage to doing math with timestamps is that your debounce function would be more accurate. A setTimeout callback can only fire when the javascript engine isn't currently busy doing something else. \$\endgroup\$ Jan 7 at 6:52
1
\$\begingroup\$

This isn't quite debouncing. It's similar, but it's not the same thing, at least not in the way debounce is commonly understood in the programming community. With debouncing, when an event is triggered, the function to run later may be delayed (if the event was triggered recently), but it's never skipped entirely. See here for an animated example of what this looks like.

To implement a debouncer, I'd either expect the function to run to have all the needed side-effects (for example, to change text on the screen). I wouldn't expect the function to return anything. In the very unusual case that it would return something, I'd expect it to return a Promise that resolves after the next time said function runs. For example, with a debounce time of 3 seconds, I'd expect this:

sayHello();
setTimeout(sayHello, 2000);
setTimeout(sayHello, 4000);
setTimeout(sayHello, 8000);

to result in the original hello function to run at millisecond 7000, and at millisecond 11000 (since both of those points are when the last call was 3 seconds earlier).

It's just terminology, but communication is important in programming. (I'd lay the blame for this at the writers of the challenge - you're just implementing the challenge, after all)

Time management I strongly agree with the other answer that calculating timestamp differences from each other and from interval mathematically seems much worse than just using setTimeout. The logic is easier to understand at a glance when you just have to clear and set a timeout.

lastTime If you were to calculate timestamp differences, since Date.now() will always return a large positive number, you could consider initializing lastTime just to 0 rather than to -Infinity.

Interval vs timeout An interval is not a timeout. While the below just so happens to work, a reader of the code could easily not expect it to:

clearInterval(timeout);
timeout = setTimeout(() => timeout = null, interval);

Use clearTimeout with setTimeout to make things clear.

This isn't functional programming - functional programming uses, among other things, pure functions (that don't rely on non-argument state). Keeping track of the last function call time or the last timeout requires state. The logic this is implementing is fundamentally stateful and impure.

\$\endgroup\$
1
\$\begingroup\$

I think the solution with the timeout variable is far more clear, and avoids arithmetic operations. However, your implementation has some odd behaviours. That is, using debounce with a high frequency will postpone the callback indefinitely.

const debounce = (func, interval) => {
    let timeout = null;
    return ()=>{
        if (timeout) return undefined;
        timeout = setTimeout( ()=>timeout=null, interval)
        return func();
    }
}

As an alternative, I propose this. Using this code, the timeout is only set when an actual function call occurs.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Adding ...args to the returned function as well as the call to func(...args) makes this more generally-usable. \$\endgroup\$
    – ggorlen
    Jan 4 at 15:56
  • 1
    \$\begingroup\$ Actually, debouncing is supposed to be able to be delayed indefinitely. You might be thinking of throttling. See css-tricks.com/debouncing-throttling-explained-examples \$\endgroup\$ Jan 7 at 6:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.