Search code examples
javascriptfor-loopsequential

Sequential-ish nature of JavaScript with a for-loop


Since JavaScript is sequential (not counting async abilities), then why does it not "seem" to behave sequential as in this simplified example:

HTML:

<input type="button" value="Run" onclick="run()"/>

JS:

var btn = document.querySelector('input');

var run = function() {
    console.clear();
    console.log('Running...');
    var then = Date.now();
    btn.setAttribute('disabled', 'disabled');

    // Button doesn't actually get disabled here!!????

    var result = 0.0;
    for (var i = 0; i < 1000000; i++) {
        result = i * Math.random();
    }

    /*
    *  This intentionally long-running worthless for-loop
    *  runs for 600ms on my computer (just to exaggerate this issue),
    *  meanwhile the button is still not disabled
    *  (it actually has the active state on it still
    *  from when I originally clicked it,
    *  technically allowing the user to add other instances
    *  of this function call to the single-threaded JavaScript stack).
    */

    btn.removeAttribute('disabled');

    /*
    *  The button is enabled now,
    *  but it wasn't disabled for 600ms (99.99%+) of the time!
    */

    console.log((Date.now() - then) + ' Milliseconds');
};

Finally, what would cause the disabled attribute not take effect until after the for-loop execution has happened? It's visually verifiable by simply commenting out the remove attribute line.

I should note that there is no need for a delayed callback, promise, or anything asynchronous; however, the only work around I found was to surround the for-loop and remaining lines in a zero delayed setTimeout callback which puts it in a new stack...but really?, setTimeout for something that should work essentially line-by-line?

What's really going on here and why isn't the setAttribute happening before the for loop runs?


Solution

  • The browser doesn't render changes to the DOM until the function returns. - @Barmar

    Per @Barmar's comments and a lot of additional reading on the subject, I'll include a summary referring to my example:

    • JavaScript is single threaded, so only one process at a time can occur
    • Rendering (repaint & reflow) is a separate/visual process that the browser performs so it comes after the function finishes to avoid the potentially heavy CPU/GPU calculations that would cause performance/visual problems if rendered on the fly

    Summarized another way is this quote from http://javascript.info/tutorial/events-and-timing-depth#javascript-execution-and-rendering

    In most browsers, rendering and JavaScript use single event queue. It means that while JavaScript is running, no rendering occurs.

    To explain it another way, I'll use the setTimeout "hack" I mentioned in my question:

    1. Clicking the "run" button puts my function in the stack/queue of things for the browser to accomplish
    2. Seeing the "disabled" attribute, the browser then adds a rendering process to the stack/queue of tasks.
    3. If we instead add a setTimeout to the heavy part of the function, the setTimeout (by design) pulls it out of the current flow and adds it to the end of the stack/queue. This means the initial lines of code will run, then the rendering of the disabled attribute, then the long-running for-loop code; all in the order of the stack as it was queued up.

    Additional resources and explanations concerning the above: