Underscore.js throttle vs debounce example

An example test visually demonstrating the difference between Underscore.js's throttle() and debounce(), the latter with the "immediate" parameter set true.

by Paulo Ávila

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<p>Click the meters repeatedly. Events throttled/filtered by 1 second.</p>
<ul class="meter meter1">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
</ul>
<ul class="meter meter2">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
</ul>
<ul class="meter meter3">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
</ul>
<ul class="meter meter4">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
</ul>
<p>
    <code>_.throttle()</code> will fire once, and subsequent clicks will fire
    only after 1 second has passed&mdash;but the subsequent click will happen.
    <b>This is not good for filtering double-clicks.</b>
</p>
<p>
    <code>_.debounce()</code> (with a <code>true</code> third parameter)
    will <i>filter</i> subsequent clicks.
    <b>This <i>is</i> good for filtering accidental clicks.</b>
</p>
<p>
    <code>_.delay()</code> will postpone the invocation of the call <i>starting from the moment it is originally clicked</i>.
</p>
<p>
    <code>_.stagger()</code> will postpone the invocation of the <i>starting from the end of the previous invocation in a series of clicks</i>. Another way to say this is that it will queue up each invocation and fire them off subsequently* after 1 second has elapsed. <b>This is very similar to <code>_.throttle</code>, except none of the invocations are dropped</b>. *Setting the third parameter to <i>false</i> will cause the invocations to NOT be sequential (which makes its behaviour equivalent to <code>_.delay</code>).
</p>

CSS

p {
    margin: 20px 20px 0px;
    clear:both;
}
.meter {
    margin: 10px 20px;
    padding: 5px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-family: sans-serif;
    display: block;
    float: left;
    clear: left;
    box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.2);
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.meter:hover {
    background: #ff8;
    cursor: pointer;
}
.meter li {
    border: 1px solid #ccc;
    background: #fff;
    width: 25px;
    height: 20px;
    text-align: center;
    padding-top: 8px;
    float: left;
    border-radius: 25px;
    margin-right: 6px;
    display: inline-block;
    color: #ccc;
}
.meter li:last-child {
    float: none;
    margin: 0;
}
.meter li.on {
    background: #2f2;
    box-shadow: inset 0px 1px 0px 2px rgba(0,0,0,0.1);
    border: 1px solid #777;
    color: #000;
    font-weight: bold;
    text-shadow: 0px 1px #fff;
}
.meter1:after {
    content: "_.throttle()";
    padding: 0px 5px;
}
.meter2:after {
    content: "_.debounce()";
    padding: 0px 5px;
}
.meter3:after {
    content: "_.delay()";
    padding: 0px 5px;
}
.meter4:after {
    content: "_.stagger()";
    padding: 0px 5px;
}
.blink:after {
    font-weight: bold;
}

JavaScript

//var staggerElapsed = 0;


var prevInvocation = 0;

$(function () {
    function changeMeter (ev) {
        var $ul = $(ev.target).closest("ul"),
            numLi = $("li", $ul).length,
            $on = $("li.on", $ul),
            numOn = $on.length;

        if (numLi == numOn) {
            // All filled! Reset!
            $on.removeClass("on");
        } else {
            // Add a new $on
            if (numOn == 0) {
                $("li:first", $ul).addClass("on");
            } else {
                $("li.on:last", $ul).next().addClass("on");
            }
        }
        
        return $ul;
    }

function rand() {
    var min = 100;
    var max = 1000;
    return (min + Math.floor(((Math.random() * max) + 1)));
}
function cb() {
    console.log('series done!');
}
var staggeredFunc = _stagger(changeMeter, 1000, cb);

var delayedFunc = function (el) {
	_.delay(changeMeter, 1000, el);
}
    
    $(".meter1").on("click", _.throttle(changeMeter, 1000));
    $(".meter2").on("click", _.debounce(changeMeter, 1000, true)); 
    $(".meter3").on("click", delayedFunc); 
    $(".meter4").on("click", staggeredFunc);
    $(".meter").on("click", function(ev) {
        var $meter = $(ev.target).closest("ul");
        $meter.addClass("blink");
        setTimeout(function () {
            $meter.removeClass("blink");
        }, 50);
    });
});

var leadingCallStamp = 0;
function _stagger(func, wait, callback) {
    var timeoutId;

//    if (!_.isFunction(func)) {
//        throw new TypeError(FUNC_ERROR_TEXT);
//    }

    function cancel() {
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        timeoutId = undefined;
    }

    function staggered() {
        var stamp = _.now();
        var context = this;
        var args = _.toArray(arguments);

        var delay = _.isFunction(wait) ? wait() : wait;
        // Normalize to a number?
        delay = delay < 0 ? 0 : (+delay || 0);
console.log(delay);
        var offset =...