为什么setTimeout(fn,0)有时有用?

我最近遇到了一个相当讨厌的bug,其中代码是通过JavaScript动态加载<select> 。 这个动态加载的<select>有一个预先选定的值。 在IE6中,我们已经有了修复所选<option>代码,因为有时候<select>selectedIndex值将与选定的<option>index属性不同步,如下所示:

field.selectedIndex = element.index;

但是,此代码无法正常工作。 即使字段的selectedIndex设置正确,错误的索引最终也会被选中。 但是,如果我在正确的时间插入alert()语句,则会选择正确的选项。 考虑到这可能是某种时机问题,我尝试了一些我之前在代码中看到的随机事件:

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

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

这工作!

我已经为我的问题找到了一个解决方案,但我不确定为什么这会解决我的问题。 有没有人有官方的解释? 通过使用setTimeout()稍后调用我的函数,我避免了什么浏览器问题?


这是有效的,因为你在做合作多任务处理。

浏览器必须一次完成许多事情,而其中一个就是执行JavaScript。 但JavaScript经常被用来做的一件事就是要求浏览器构建一个显示元素。 这通常被认为是同步完成的(特别是JavaScript不是并行执行的),但不能保证是这种情况,并且JavaScript没有明确的等待机制。

解决方案是“暂停”JavaScript执行以使渲染线程赶上。 这是setTimeout()的超时时间为0的效果。 它就像C中的线程/进程产量。虽然它似乎说“立即运行”,但它实际上使浏览器有机会完成一些非JavaScript事情,这些事情在参加这个新的JavaScript之前一直等待完成。

(实际上, setTimeout()在执行队列的末尾重新排队新的JavaScript。请参阅注释以获取更长的解释。)

IE6碰巧更容易出现这种错误,但我发现它出现在较旧版本的Mozilla和Firefox中。


前言:

重要提示:虽然它最高评价并被接受,但@staticsan接受的答案实际上并不正确! - 请参阅David Mulder的解释原因。

其他一些答案是正确的,但实际上并没有说明问题得到了解决,所以我创建了这个答案来呈现详细的说明。

因此,我发布了浏览器详细步骤以及如何使用setTimeout()有所帮助 。 它看起来很长,但实际上非常简单直接 - 我只是说得非常详细。

更新:我已经让一个JSFiddle生活 - 演示下面的解释:http://jsfiddle.net/C2YBE/31/。 非常感谢 @ TengChung帮助启动它。

UPDATE2:为了防止JSFiddle网站死亡或删除代码,我在最后添加了代码到这个答案。


详情

想象一下带有“做某事”按钮和结果div的网络应用程序。

“do something”按钮的onClick处理函数调用一个函数“LongCalc()”,它有两件事:

  • 做一个很长的计算(比如说需要3分钟)

  • 将计算结果打印到结果div中。

  • 现在,你的用户开始测试这个,点击“做某事”按钮,并且页面坐在那里看起来没有什么3分钟,他们不安,再次点击按钮,等待1分钟,没有任何反应,再次点击按钮...

    问题很明显 - 你需要一个“状态”DIV,它显示了正在发生的事情。 让我们看看它是如何工作的。


    所以你添加一个“Status”DIV(最初为空),并修改onclick处理函数(函数LongCalc() )来做4件事情:

  • 将状态“计算...可能需要〜3分钟”填入状态DIV

  • 做一个很长的计算(比如说需要3分钟)

  • 将计算结果打印到结果div中。

  • 将状态“计算完成”填入状态DIV

  • 而且,你很乐意让应用程序给用户重新测试。

    他们非常生气地回到你身边。 并解释当他们点击按钮时, 状态DIV从未更新过“正在计算...”状态!


    你抓住你的脑袋,问StackOverflow(或阅读文档或谷歌),并认识到这个问题:

    浏览器将所有事件产生的“TODO”任务(UI任务和JavaScript命令)放入单个队列中 。 不幸的是,用新的“计算...”值重新绘制“状态”DIV是一个单独的TODO,它将排队到队尾!

    以下是用户测试期间的事件细目,每个事件之后的队列内容:

  • 队列: [Empty]
  • 事件:点击按钮。 事件后排队: [Execute OnClick handler(lines 1-4)]
  • 事件:在OnClick处理程序中执行第一行(例如,更改状态DIV值)。 事件后排队: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]请注意,尽管DOM变化瞬间发生,但要重新绘制相应的DOM元素,您需要一个由DOM更改触发的新事件,该事件在队列末尾处执行
  • 问题!!! 问题!!! 详情如下。
  • 事件:执行处理程序的第二行(计算)。 排队后: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • 事件:执行处理程序中的第3行(填充结果DIV)。 排队后: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
  • 事件:执行处理程序中的第4行(使用“完成”填充状态DIV)。 队列: [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]
  • 事件:从onclick处理程序子执行隐式return 。 我们将“Execute OnClick处理程序”从队列中取出,并开始执行队列中的下一个项目。
  • 注:由于我们已经完成了计算,所以用户已经过了3分钟。 重新抽奖活动还没有发生!
  • 事件:用“计算”​​值重新绘制状态DIV。 我们进行重新抽签并将其从队列中取出。
  • 事件:用结果值重新绘制结果DIV。 我们进行重新抽签并将其从队列中取出。
  • 事件:用“完成”值重新绘制状态DIV。 我们进行重新抽签并将其从队列中取出。 敏锐的观众甚至可能会注意到“Status DIV with”Calculating“数值闪烁几微秒 - 计算完成后
  • 因此,潜在的问题是,“状态”DIV的重新绘制事件被放置在队列中的末尾,在“执行线2”事件需要3分钟后,因此实际的重新绘制不会发生,直到计算完成后。


    setTimeout()来解救。 它有什么帮助? 因为通过setTimeout调用长执行代码,你实际上创建了2个事件: setTimeout执行本身,和(由于0超时),为正在执行的代码分开队列条目。

    因此,为了解决您的问题,您将您的onClick处理程序修改为TWO语句(在新函数中或onClick的块):

  • 将状态“计算...可能需要〜3分钟”填入状态DIV

  • 用0超时执行setTimeout()并调用LongCalc()函数

    LongCalc()函数几乎与上次相同,但显然没有“计算...”状态DIV更新作为第一步; 而是立即开始计算。

  • 那么,事件序列和队列现在是什么样子呢?

  • 队列: [Empty]
  • 事件:点击按钮。 事件后排队: [Execute OnClick handler(status update, setTimeout() call)]
  • 事件:在OnClick处理程序中执行第一行(例如,更改状态DIV值)。 事件后排队: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
  • 事件:在处理程序中执行第二行(setTimeout调用)。 排队后: [re-draw Status DIV with "Calculating" value] 。 队列中没有任何新东西再持续0秒。
  • 事件:超时后的警报在0秒后熄灭。 排队后: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
  • 事件: 用“计算”​​值重新绘制状态DIV 。 排队后: [execute LongCalc (lines 1-3)] 。 请注意,这个重新绘制事件可能实际发生在闹钟响起之前,这也同样适用。
  • ...
  • 万岁! 状态DIV在计算开始之前刚刚更新为“计算...”!



    下面是来自JSFiddle的示例代码,演示了这些示例:http://jsfiddle.net/C2YBE/31/:

    HTML代码:

    <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代码:(在onDomReady执行,可能需要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);
    });
    

    看看John Resig关于JavaScript定时器如何工作的文章。 当你设置超时时,它实际上将异步代码排队,直到引擎执行当前的调用栈。

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

    上一篇: Why is setTimeout(fn, 0) sometimes useful?

    下一篇: Get difference between date returns zero