2
\$\begingroup\$

I've built a function that mimics _.throttle (returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds).

I would like to know what you think about my code, especially if the code is clean and essential and if i'm managing arguments correctly.

function throttle(func, interval){
   var toExecute = true,
       queue = false;
   var result;
   return function doThrottle(){
       var args = Array.prototype.slice.call(arguments);

       if(toExecute){
           toExecute = false;
           queue = false;
           result = func.apply(null, args);
           startTimer(interval);
       } else {
           queue = true;
       }

       function startTimer(interval){
           setTimeout(function(){
               toExecute = true;
               if(queue){
                   result = func.apply(null, args);
               }
            }, interval);
       }

       return result;

   }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Your code isn't quite right, as far as I can tell. In the snippet below, you can compare it to Underscore's version.

function throttle(func, interval){
   var toExecute = true,
       queue = false;
   var result;
   return function doThrottle(){
       var args = Array.prototype.slice.call(arguments);

       if(toExecute){
           toExecute = false;
           queue = false;
           result = func.apply(null, args);
           startTimer(interval);
       } else {
           queue = true;
       }

       function startTimer(interval){
           setTimeout(function(){
               toExecute = true;
               if(queue){
                   result = func.apply(null, args);
               }
            }, interval);
       }

       return result;
   }
}


var count = 0;

function printSomething(elementId, count) {
  document.getElementById(elementId).innerHTML += String(count) + "<br>";
}

var opThrottledPrint = throttle(printSomething, 2000),
    underscoreThrottledPrint = _.throttle(printSomething, 2000);

function invoke() {
  count++;
  opThrottledPrint("op", count);
  underscoreThrottledPrint("underscore", count);
}
<script src="http://underscorejs.org/underscore-min.js"></script>

<button type="button" onclick="invoke()">Click me a few times</button>

<p>Printing is throttled by 2 seconds</p>

<table>
  <tr>
    <th>OP's code</td>
    <th>Underscore</td>
  </tr>
  <tr>
    <td valign="top" id="op"></td>
    <td valign="top" id="underscore"></td>
  </tr>
</table>

Three things to note:

  1. If you click twice in row, your throttler does introduce the right delay, but it reuses arguments from the first call when executing the throttled call. So in the snippet's example, your function will cause the same number to be printed twice, despite being called with a new number on each click. I'd consider this a pretty major bug.

    The reason for all of this is the curious structure of your code. You set a timeout that's poised to repeat the current call - unless queue is false when the timer runs out. So your code actually creates a sort of "conditional repeater" rather than a throttler.

    Again, this is just a strange construction.

  2. If you click twice, and then click a third time as soon as both of the previous calls have executed, the third call executes immediately. In other words, if nothing's "queued" the call is handled immediately, even if it's been less than the delay-time since the most recent invocation.

    To attempt some ascii illustration of the behavior of the snippet above:

    Time ------->
    
    click . click . . . . . . . . . . . . . . . . . . . click . . . .
      |        \__________________________________        |      
      |                      timeout              \       |
    print . . . . . . . . . . . . . . . . . . . print . print . . . .
                            ^                         ^
                      Correct delay               No delay!
    

    Again, I'd consider this a major bug since your function is not actually throttling anything in that case.

  3. Returning anything from the throttled function is pointless. Sure, if it executes immediately it will return whatever the wrapped function returns. But if it doesn't execute immediately due to throttling, things get weird.

    The wrapper function itself returns immediately; if you set a timeout, it doesn't wait for it to run out. So setting result in the timed function happens long after result has already been returned.

    But because result is a closure variable, it's actually worse than a common async mistake: If your call gets delayed/throttled, the previous invocation's return value gets returned.

    Like the bug with reusing arguments, this is quite bad if you're expecting to be able to use return values. But, as mentioned, it's impossible to consistently provide return values, when the function may just return before the throttled code has run. So you're better off not returning anything at all.

So with your code, you get:

  • Delayed repetition instead of actual throttling.
  • Risk of no throttling at all.
  • Risk of incorrect return values.

Not really ideal.


I'd do something like this:

function throttle(func, delay) {
  var timer = null,
      queued = null;

  // simple function to introduce a delay
  function delay() {
    timer = setTimeout(resume, delay);
  }

  // end-of-delay handler
  function resume() {
    timer = null;
    if(queued) queued(); // call the queued invocation, if any
    queued = null;
  }

  return function () {
    var args, invocation;

    if(queued) return; // if something's already queued, stop here

    // otherwise get the args...
    args = [].slice.call(arguments);

    // and create a function to apply them
    invocation = function () {
      func.apply(null, args);
      delay(); // make sure any invocation will always cause a delay
    };

    if(!timer) {
      // if there's no timer, invoke immediately
      invocation();
    } else {
      // otherwise enqueue for later
      queued = invocation;
    }
  }
}

You can try it out below. It seems to behave the same as Underscore's.

function throttle(func, interval){
   var toExecute = true,
       queue = false;
   var result;
   return function doThrottle(){
       var args = Array.prototype.slice.call(arguments);

       if(toExecute){
           toExecute = false;
           queue = false;
           result = func.apply(null, args);
           startTimer(interval);
       } else {
           queue = true;
       }

       function startTimer(interval){
           setTimeout(function(){
               toExecute = true;
               if(queue){
                   result = func.apply(null, args);
               }
            }, interval);
       }

       return result;
   }
}

function throttle2(func, delay) {
  var timer = null,
      queued = null;

  function delay() {
    timer = setTimeout(resume, delay);
  }
  
  function resume() {
    timer = null;
    if(queued) queued();
    queued = null;
  }

  return function () {
    var args, invocation;
    
    if(queued) return;
    
    args = [].slice.call(arguments);
    
    invocation = function () {
      func.apply(null, args);
      delay();
    };
    
    if(!timer) {
      invocation();
    } else {
      queued = invocation;
    }
  }
}


var count = 0;

function printSomething(elementId, count) {
  document.getElementById(elementId).innerHTML += String(count) + "<br>";
}

var opThrottledPrint = throttle(printSomething, 2000),
    underscoreThrottledPrint = _.throttle(printSomething, 2000),
    reviewThrottledPrint = _.throttle(printSomething, 2000);

function invoke() {
  count++;
  opThrottledPrint("op", count);
  underscoreThrottledPrint("underscore", count);
  reviewThrottledPrint("review", count);
}
<script src="http://underscorejs.org/underscore-min.js"></script>

<button type="button" onclick="invoke()">Click me a few times</button>

<p>Printing is throttled by 2 seconds</p>

<table>
  <tr>
    <th>OP's code</td>
    <th>Underscore</td>
    <th>Review code</td>
  </tr>
  <tr>
    <td valign="top" id="op"></td>
    <td valign="top" id="underscore"></td>
    <td valign="top" id="review"></td>
  </tr>
</table>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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