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;
 }
}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jun 3, 2015 at 7:31
\$\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>

answered Jun 5, 2015 at 23:57
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.