All Posts

Deconstructing throttle in underscore.js

Ever try to build a rate limiter? underscore.js has one called throttle. And like most utility functions, its source code on the surface looks a bit dense. Teasing it apart piece by piece helps us see how it accomplishes its purpose. In plain language:

“We have a browser event that fires constantly, but we don’t want our event handler function to fire constantly, too — only once per second, max.”

The Big Picture

Imagine we want to build a clever new app that writes to the browser console the user’s mouse cursor coordinates as they move the mouse.

function announceCoordinates (e) {
    console.log('cursor at ' + e.clientX + ', ' + e.clientY);
}
window.addEventListener('mousemove', announceCoordinates);

Brilliant!

Except, as the user wanders about, mousemove fires constantly. Maybe we’re announcing the coordinates too often? It’s arguable. And if instead of console logging we were telling our server what the mouse coordinates are, we’d definitely be sending word to home base way, way too often.

So — we need a rate limiter.

Slowing Our Roll

Let’s rewrite our event binding to take advantage of throttle from underscore.js.

window.addEventListener(
    'mousemove',
    _.throttle(announceCoordinates, 1000)
);

And just like that, our announcements only broadcast once per second.

How It Works

In our original event binding, we configured it to call announceCoordinates. But the second time around, we give it _.throttle(announceCoordinates, 1000).That looks more like we’re calling a function than pointing at one, doesn’t it?

In fact, we are calling throttle here, passing into it two parameters: our function name and the throttle time in milliseconds. It then does its magic and ultimately returns a function. It’s that resulting function that our event binding registers.

Take a look at the source code for throttle and find where it returns the new function. (For the sake of simplicity, the options param and associated logic have been removed.)

_.throttle = function (func, wait) {
    var context, args, result;
    var timeout = null;
    var previous = 0;
    
    var later = function () {
        previous = _.now();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };
    return function () {
        var now = _.now();
        if (!previous) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
        return result;
    };
};

Yup, line 12.

Calling throttle in essence configures a brand new function that wraps around our original one. This new function is what’s registered to the event handler. And yes, the returned function will get called constantly by our friend mousemove. But dutifully, it protects our logging function and keeps track of when it should fire.

Which leads to …

The Hardest Part

We see above setTimeout is being used inside throttle. Anyone familiar with JavaScript has probably used it at some point, too. “Run this code 5000ms from now!!”, they likely exclaim.

setTimeout by itself can’t rate limit — it would simply delay the inevitable flood of function calls. But in the new wrapper around our function, things get clever. As it gets called over and over and over again, it goes through this routine:

  • Check how much time has passed
  • If enough time has passed, call our function ❤
  • If we still need to wait, set a reminder called later. That’s nothing more than our function in a setTimeout call. It only sets one of these! If that reminder already exists, nothing happens.

Eventually, our function gets called one of two ways:

  • Automatically, by the later reminder, or
  • Directly, if the timing is just right (line 18). And if that happens, the reminder is cleared.

And around and around we go.

Extra Credit

  • Use console.log or web inspector breakpoints to watch the function as it works.
  • Check out the annotated source to see about that mysterious options parameter.
  • Try rewriting throttle in the lovely new ES6 syntax.

Originally posted on the author’s personal Medium. Reposted here with permission.