Saturday, March 14, 2009

How to do a while-sleep loop in Javascript

There is no sleep() function in Javascript. It makes sense, really, because Javascript is single threaded. So, a sleep would freeze the entire browser until it returned. But, being able to sleep before continuing processing is a very common need when programming.

The example that comes immediately to mind is polling. When polling a resource, one would naturally do so in a while loop, exiting when the conditions are right for processing to continue.

But how can this be done in Javascript gracefully if there is no way to sleep? You could poll without a sleep, but that would consume all of the browser's processing cycles, effectively locking it up, which is probably not what you would want. Well, here is how to simulate a while-sleep loop in Javascript.
The only way to introduce time delays is to use timeouts. This is usually a crappy fallback solution because you end up breaking the logic out into little chunks that go into separate functions that get called by a confusing arrangement of timeout callbacks, completely breaking the flow of the code for the reader...

Because Javascript is so flexible, a sleep can be simulated in Javascript. And it can be done in a way that maintains the flow of the logic in the source, making it easier to understand by maintaining the structure of the original while loop.

The examples here can be run right in your browser. They just just "count up" at the end of the document. The first has no sleep (since it doesn't exist), so it happens instantaneously. The second counts more slowly with a simulated sleep.

Here's the example a while loop.
actionResultOne = "init";
function action() { return actionResultOne; }

// quick-n-dirty logger
echo = function(msg) {
    var m = document.createElement('DIV');
    m.innerHTML = msg;
    document.body.appendChild( m );
}

function doWhileSleep() {

    var resultOne = 'init';
    var attempts = 0;

    echo( "executing loop..." );

    while ( attempts < 10 && "init" == resultOne ) {
        attempts++;
        echo( "Performing action...  attempt " + attempts );
        resultOne = action();
        if ("done" == resultOne) {
            break;  // Stop looping on success
        }
        else if ("error" == resultOne) {
            return; // Exit on error.  No post-while processing
        }

        // Wait before trying again, so we don't consume the processor
        // sleep( 1000 );   // :(  doesn't exist

    } // end while

    // Now continue processing
    echo( "post-while processing..." );
}


To add a delay to the while processing, do the following:
  1. Put the entire while loop into an anonymous function that calls itself for each "iteration" of the loop.
  2. Put everything that comes after the while into a "post while" function.
  3. At the end of the while loop's logic, add a call to the "post while" function
  4. In the while loop, where the sleep would go, add timeout that calls the while loop's anonymous function again
  5. Add a 'return' after the timeout (so the "post while" function doesn't get called prematurely.
  6. Voila!
Now lets introduce a sleep, so the iterations of the while loop can be executed with a delay. The changes are highlighted, so you can see that the logical structure of the while hasn't changed:

actionResultOne = "init";
function action() { return actionResultOne; }

// quick-n-dirty logger
echo = function(msg) {
    var m = document.createElement('DIV');
    m.innerHTML = msg;
    document.body.appendChild( m );
}

function doWhileSleepInterval() {

    var resultOne = 'init';
    var attempts = 0;

    echo( "executing loop..." );

    /*
     * The entire while loop is wrapped in an anonymous function that is
     * invoked on a timeout.  The initial timeout is 0, so that the post-while
     * function can be set.
     *
     * ALL of the logic following the while loop will be wrapped in a
     * "post-while" object.  This object is a closure, allowing the while-body
     * timeout function to call it when the while loop is complete.
     */
    var postWhile = { func : undefined };
    setTimeout(function() {
        while ( attempts < 10 && "init" == resultOne ) {
            attempts++;
            echo( "Performing action...  attempt " + attempts );
            resultOne = action();
            if ("done" == resultOne) {
                break;  // Stop looping on success
            }
            else if ("error" == resultOne) {
                return; // Exit on error.  No post-while processing
            }

            // Wait before trying again, so we don't consume the processor
            // sleep( 1000 );   // :(  doesn't exist

            {
                /*
                 * This replaces the implied 'continue' at the END of the while
                 * loop.  The 'while' function is executed again, simulating
                 * another iteration of the loop, and the post-processing logic
                 * is not run.
                 *
                 * We don't really need an actual 'while' anymore, but this
                 * helps clarify the flow of logic for the reader, making it
                 * one less thing that differs from a plain ol' while loop.
                 */
                setTimeout( arguments.callee, 500 );
                return;// always return.  Next iteration is run by the setTimeout()
            }
        } // end while

        /*
         * ALL of the logic that follows the while goes into the post-while
         * function, which is executed on break, or when the while condition
         * fails.  This flow is exactly as it would be for a normal while loop.
         */
        postWhile.func();
    }, 0);
    /*
     * Wrap ALL of the original post-processing code in the post-proc object
     */
    postWhile.func = function() {
        // Now continue processing
        echo( "post-while processing..." );
    }
}

This particular solution won't work in all cases. For example, the top level function can't return a value, because the while loop doesn't actually run in that function. But that can be solved easily enough by sending a 'callback' function in as an argument.

Hope this helps!


No comments: