jbolanos jbolanos - 4 months ago 23
Javascript Question

jQuery UI progressbar on a timer

I have a progressbar on the top of my page that I'm using to display the 1 minute timer used to refresh all the charts. The problem I'm having is that it never seems to be consistent.

function headerBar() {
var progressbar = $('#timerBar');
progressbar.progressbar({
value: 0
});

function progress() {
var val = progressbar.progressbar('value') || 0;
progressbar.progressbar('value', val + 5);
if (val < 99) {
setTimeout(progress, 3000);
}
}
progress();
}
headerBar();
setInterval(function () {
headerBar();
}, 60 * 1000);


The first time it runs fine, but the 2nd time seems to take 1/2 as long and every time after that seems to shave off time from the count.

here's the fiddle

Answer

Hurray! You're running into a kind-of race condition. Since setTimeout() does not guarantee to call every 3000ms like setInterval() (and even if it did, you'd still be subject to this problem), what most likely happens is that your interval calls headerBar() before progress() manages to stop itself from adding more timeouts! Here's a timeline to clear this up:

Your progressbar is at 95!

timeout1 -> I better call progress()!

progress() -> Set progress bar to val + 5 = 100! val(95) < 99 ? Yeah! I better set a timeout to call myself again

interval -> Thy time is up! headerBar()!

headerBar() -> I set the progress to 0, and start more progress()!

progress() -> Set progress bar to 5! Timeout to call myself again!

progress() -> Set progress bar to 10! Timeout to call myself again! (Oops, I never got to val > 99 to stop calling myself repeatedly!)

time passes...

interval -> It's time to go FASTER! headerBar()!

But how do you solve this problem? Well, firstly, you'll want to change your progress() check to use val + 5 < 99. That still won't save you from problems like calling 20 timeouts taking longer than your one interval - you'll need some way to signal your progress() to stop with all the progressing.

One way to do this would be to add some sanity checks to headerBar():

function headerBar() {
    var progressbar = $('#timerBar');
    var lastProgress = 0;
    progressbar.progressbar({
        value: lastProgress
    });

    function progress() {
        var val = progressbar.progressbar('value') || 0;
        if (val != lastProgress) {
            // Someone's modified the bar! I best stop before I do too much progress!
            return;
        }
        progressbar.progressbar('value', val + 5);
        if (val + 5 < 99) {
            setTimeout(progress, 3000);
        }
    }
    progress();
}

This is a very hacky solution, however. A much more elegant one would be to use a closure!

var headerBar = (function () {
    var inProgress = false;
    var progressbar = $('#timerBar'); // Save some trees by calling jQuery selection only once!
    return function headerBar() {
        if (inProgress) {
            clearTimeout(inProgress);
            inProgress = false;
        }
        progressbar.progressbar({
            value: 0
        });
        function progress() {
            var val = progressbar.progressbar('value') || 0;
            progressbar.progressbar('value', val + 5);
            if (val < 99) {
                inProgress = setTimeout(progress, 3000);
            }
        }
        progress();
    }
})();

In general though, you could still work on eliminating your interval(this would introduce a slight error in your timing), by modifying headerBar() to simply tell you when it's done.

// Pass the jQuery element so you not only have to select it once, but don't depend on the headerBar knowing where to look for it.
var headerBar = (function (progressbar) {
    return function headerBar(callback) {
        progressbar.progressbar({ value : 0 });
        (function progress() {
            var newVal = (progressbar.progressbar('value') || 0) + 5;
            progressbar.progressbar('value', newVal);
            if (newVal < 99) setTimeout(progress, 3000);
            else callback();
        })();
    }
})($('#timerBar'));

function heavyLifting() {
    // get stuff, etc
    headerBar(heavyLifting);
}
Comments