views:

107

answers:

6

I have a JavaScript function which is quite long and performs a number of tasks, I would like to report progress to the user by updating the contents of a SPAN element with a message as I go. I tried adding document.getElementById('spnProgress').innerText = ... statements throughout the function code.

However, whilst the function is executing the UI will not update and so you only ever see the last message written to the SPAN which is not very helpful.

My current solution is to break the task up into a number of functions, at the end of each I set the SPAN message and then "trigger" the next one with a window.setTimeout call with a very short delay (say 10ms). This yields control and allows the browser to repaint the SPAN with the updated message before starting the next step.

However I find this very messy and difficult to follow the code, I'm thinking there must be a better way. Does anyone have any suggestions? Is there any way to force the SPAN to repaint without having to leave the context of the function?

Thanks

+6  A: 

The way you're doing it is the right way (at the moment, this may change as standards emerge and are adopted [see Andrew Aylett's answer], but not for a while yet). You have to yield like that to allow the browser to do its UI updates. I've found that the more I think like this, the cleaner things get, but my first few stabs at doing it were indeed quite "messy." Hopefully you find the same thing as you get used to it.

T.J. Crowder
This is the right choice.
Kangkan
A: 

That's like asking whether it's possible to interrupt a procedure without interrupting a procedure. The answer is no. You have to use a setTimeout() or setInterval() to pass control to the browser's rendering engine.

If you use setInterval() you can get that process going and in your executing function simply update an external variable, which will be polled by the function called by setInterval(). That way you only have to make one call instead of doing them in a loop.

Robusto
A: 

Not that I know of. You could break up your code in such a way that the individual functions can share variables, thus:

var a = some_local_state();
runTasksWithProgress([
    function() {
        do_some_work(a);
        a = a + 1;
    },
    function() {
        do_some_other_work(a);
        a = a * 2;
    },
    ...
    ]);

runTasksWithProgress is a bit tricky. You would basically invoke the first task, update the status, then set up a call-back to run subsequent tasks.

This approach might alleviate some of the pain.

Marcelo Cantos
+1  A: 

You need to be aware that in certain browsers you will receive a script timeout message if you have a long running script. So it is actually desirable to split this using a timer.

Having said this, if you are looking for a really structured way of doing this, then please have a look at the jQuery map reduce plugin which allows you to map functions to a set of data and control how these functions are executed (on a timer etc)

http://plugins.jquery.com/project/MapReduce

James Westgate
+1 good point about the script timeout.
T.J. Crowder
A: 

Something like this may work if your function's work is performed in a loop. It checks the amount of time that has passed and updates the progress bar if 1/2 second has gone by. (The example is untested. So you may need to play with it a bit.)

var start;
function longRunning(lastState){
    start = (new Date);
    for(var i = lastState; i < 1e6 /*= 1000000 iterations */; ++i){
         if((new Date)-start<500){
             // do your stuff;
         }
         else{
             // call a function to update the progress bar
             updateProgressBar();
             // continue the loop...
             setTimeout(function(){longRunning(i);},13);
             break;
         }
    }

}
longRunning(0);
David Murdoch
+1  A: 

If you've got control of the target browser, you may be able to use an HTML5 worker thread to do the work in the background.

Andrew Aylett