Why is setTimeout(fn, 0) sometimes useful?

I've recently run into a rather nasty bug, wherein the code was loading a <select> dynamically via JavaScript. This dynamically loaded <select> had a pre-selected value. In IE6, we already had code to fix the selected <option> , because sometimes the <select> 's selectedIndex value would be out of sync with the selected <option> 's index attribute, as below:

field.selectedIndex = element.index;

However, this code wasn't working. Even though the field's selectedIndex was being set correctly, the wrong index would end up being selected. However, if I stuck an alert() statement in at the right time, the correct option would be selected. Thinking this might be some sort of timing issue, I tried something random that I'd seen in code before:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

And this worked!

I've got a solution for my problem, but I'm uneasy that I don't know exactly why this fixes my problem. Does anyone have an official explanation? What browser issue am I avoiding by calling my function "later" using setTimeout() ?


This works because you're doing co-operative multi-tasking.

A browser has to do a number of things pretty much all at once, and just one of those is execute JavaScript. But one of the things JavaScript is very often used for is to ask the browser to build a display element. This is often assumed to be done synchronously (particularly as JavaScript is not executed in parallel) but there is no guarantee this is the case and JavaScript does not have a well-defined mechanism for waiting.

The solution is to "pause" the JavaScript execution to let the rendering threads catch up. And this is the effect that setTimeout() with a timeout of 0 does. It is like a thread/process yield in C. Although it seems to say "run this immediately" it actually gives the browser a chance to finish doing some non-JavaScript things that have been waiting to finish before attending to this new piece of JavaScript.

(In actuality, setTimeout() re-queues the new JavaScript at the end of the execution queue. See the comments for links to a longer explanation.)

IE6 just happens to be more prone to this error, but I have seen it occur on older versions of Mozilla and in Firefox.


Preface:

IMPORTANT NOTE: While it's most upvoted and accepted, the accepted answer by @staticsan actually is NOT CORRECT! - see David Mulder's comment for explanation why.

Some of the other answers are correct but don't actually illustrate what the problem being solved is, so I created this answer to present that detailed illustration.

As such, I am posting a detailed walk-through of what the browser does and how using setTimeout() helps . It looks longish but is actually very simple and straightforward - I just made it very detailed.

UPDATE: I have made a JSFiddle to live-demonstrate the explanation below: http://jsfiddle.net/C2YBE/31/ . Many thanks to @ThangChung for helping to kickstart it.

UPDATE2: Just in case JSFiddle web site dies, or deletes the code, I added the code to this answer at the very end.


DETAILS :

Imagine a web app with a "do something" button and a result div.

The onClick handler for "do something" button calls a function "LongCalc()", which does 2 things:

  • Makes a very long calculation (say takes 3 min)

  • Prints the results of calculation into the result div.

  • Now, your users start testing this, click "do something" button, and the page sits there doing seemingly nothing for 3 minutes, they get restless, click the button again, wait 1 min, nothing happens, click button again...

    The problem is obvious - you want a "Status" DIV, which shows what's going on. Let's see how that works.


    So you add a "Status" DIV (initially empty), and modify the onclick handler (function LongCalc() ) to do 4 things:

  • Populate the status "Calculating... may take ~3 minutes" into status DIV

  • Makes a very long calculation (say takes 3 min)

  • Prints the results of calculation into the result div.

  • Populate the status "Calculation done" into status DIV

  • And, you happily give the app to users to re-test.

    They come back to you looking very angry. And explain that when they clicked the button, the Status DIV never got updated with "Calculating..." status!!!


    You scratch your head, ask around on StackOverflow (or read docs or google), and realize the problem:

    The browser places all its "TODO" tasks (both UI tasks and JavaScript commands) resulting from events into a single queue . And unfortunately, re-drawing the "Status" DIV with the new "Calculating..." value is a separate TODO which goes to the end of the queue!

    Here's a breakdown of the events during your user's test, contents of the queue after each event:

  • Queue: [Empty]
  • Event: Click the button. Queue after event: [Execute OnClick handler(lines 1-4)]
  • Event: Execute first line in OnClick handler (eg change Status DIV value). Queue after event: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value] . Please note that while the DOM changes happen instantaneously, to re-draw the corresponding DOM element you need a new event, triggered by the DOM change, that went at the end of the queue .
  • PROBLEM!!! PROBLEM!!! Details explained below.
  • Event: Execute second line in handler (calculation). Queue after: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value] .
  • Event: Execute 3rd line in handler (populate result DIV). Queue after: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result] .
  • Event: Execute 4th line in handler (populate status DIV with "DONE"). Queue: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] .
  • Event: execute implied return from onclick handler sub. We take the "Execute OnClick handler" off the queue and start executing next item on the queue.
  • NOTE: Since we already finished the calculation, 3 minutes already passed for the user. The re-draw event didn't happen yet!!!
  • Event: re-draw Status DIV with "Calculating" value. We do the re-draw and take that off the queue.
  • Event: re-draw Result DIV with result value. We do the re-draw and take that off the queue.
  • Event: re-draw Status DIV with "Done" value. We do the re-draw and take that off the queue. Sharp-eyed viewers might even notice "Status DIV with "Calculating" value flashing for fraction of a microsecond - AFTER THE CALCULATION FINISHED
  • So, the underlying problem is that the re-draw event for "Status" DIV is placed on the queue at the end, AFTER the "execute line 2" event which takes 3 minutes, so the actual re-draw doesn't happen until AFTER the calculation is done.


    To the rescue comes the setTimeout() . How does it help? Because by calling long-executing code via setTimeout , you actually create 2 events: setTimeout execution itself, and (due to 0 timeout), separate queue entry for the code being executed.

    So, to fix your problem, you modify your onClick handler to be TWO statements (in a new function or just a block within onClick ):

  • Populate the status "Calculating... may take ~3 minutes" into status DIV

  • Execute setTimeout() with 0 timeout and a call to LongCalc() function .

    LongCalc() function is almost the same as last time but obviously doesn't have "Calculating..." status DIV update as first step; and instead starts the calculation right away.

  • So, what does the event sequence and the queue look like now?

  • Queue: [Empty]
  • Event: Click the button. Queue after event: [Execute OnClick handler(status update, setTimeout() call)]
  • Event: Execute first line in OnClick handler (eg change Status DIV value). Queue after event: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value] .
  • Event: Execute second line in handler (setTimeout call). Queue after: [re-draw Status DIV with "Calculating" value] . The queue has nothing new in it for 0 more seconds.
  • Event: Alarm from the timeout goes off, 0 seconds later. Queue after: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)] .
  • Event: re-draw Status DIV with "Calculating" value . Queue after: [execute LongCalc (lines 1-3)] . Please note that this re-draw event might actually happen BEFORE the alarm goes off, which works just as well.
  • ...
  • Hooray! The Status DIV just got updated to "Calculating..." before the calculation started!!!



    Below is the sample code from the JSFiddle illustrating these examples: http://jsfiddle.net/C2YBE/31/ :

    HTML code:

    <table border=1>
        <tr><td><button id='do'>Do long calc - bad status!</button></td>
            <td><div id='status'>Not Calculating yet.</div></td>
        </tr>
        <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
            <td><div id='status_ok'>Not Calculating yet.</div></td>
        </tr>
    </table>
    

    JavaScript code: (Executed on onDomReady and may require jQuery 1.9)

    function long_running(status_div) {
    
        var result = 0;
        // Use 1000/700/300 limits in Chrome, 
        //    300/100/100 in IE8, 
        //    1000/500/200 in FireFox
        // I have no idea why identical runtimes fail on diff browsers.
        for (var i = 0; i < 1000; i++) {
            for (var j = 0; j < 700; j++) {
                for (var k = 0; k < 300; k++) {
                    result = result + i + j + k;
                }
            }
        }
        $(status_div).text('calculation done');
    }
    
    // Assign events to buttons
    $('#do').on('click', function () {
        $('#status').text('calculating....');
        long_running('#status');
    });
    
    $('#do_ok').on('click', function () {
        $('#status_ok').text('calculating....');
        // This works on IE8. Works in Chrome
        // Does NOT work in FireFox 25 with timeout =0 or =1
        // DOES work in FF if you change timeout from 0 to 500
        window.setTimeout(function (){ long_running('#status_ok') }, 0);
    });
    

    Take a look at John Resig's article about How JavaScript Timers Work. When you set a timeout, it actually queues the asynchronous code until the engine executes the current call stack.

    链接地址: http://www.djcxy.com/p/10124.html

    上一篇: 框架巴斯特巴斯特...需要巴斯特代码

    下一篇: 为什么setTimeout(fn,0)有时有用?