协慌网

登录 贡献 社区

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

我最近遇到了一个相当讨厌的错误,其中代码是通过 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 中。


请参阅 Philip Roberts 的演讲“事件循环到底是什么?”有更详尽的解释。

前言:

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

其他一些答案是正确的,但实际上没有说明要解决的问题是什么,所以我创建了这个答案,以提供详细的说明。

因此,我将详细介绍浏览器的功能以及使用setTimeout()如何帮助 。它看起来很长,但实际上非常简单明了 - 我只是非常详细。

更新:我已经制作了一个 JSFiddle 来演示以下解释: http//jsfiddle.net/C2YBE/31/ 。非常感谢 @ThangChung 帮助启动它。

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


细节

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

“执行某事” 按钮的onClick处理程序调用函数 “LongCalc()”,它执行两项操作:

  1. 做了很长的计算(比如需要 3 分钟)

  2. 将计算结果打印到结果 div 中。

现在,你的用户开始测试这个,点击 “做某事” 按钮,页面就在那里做 3 分钟看似没事,他们变得焦躁不安,再次点击按钮,等待 1 分钟,没有任何反应,再次点击按钮......

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


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

  1. 将状态 “计算... 可能需要约 3 分钟” 填充到状态 DIV 中

  2. 做了很长的计算(比如需要 3 分钟)

  3. 将计算结果打印到结果 div 中。

  4. 将 “已完成计算” 状态填充到状态 DIV 中

并且,您乐意将应用程序提供给用户重新测试。

他们回到你身边看起来很生气。并解释当他们点击按钮时, 状态 DIV 永远不会更新 “计算...” 状态!


你挠头,在 StackOverflow(或阅读文档或谷歌)上四处询问,并意识到问题:

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

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

  • 队列: [Empty]
  • 事件:单击按钮。事件后的队列: [Execute OnClick handler(lines 1-4)]
  • 事件:在 OnClick 处理程序中执行第一行(例如,更改 Status 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 行(使用 “DONE” 填充状态 DIV)。队列: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • 事件:从onclick处理程序 sub 执行隐含的return 。我们从队列中取出 “Execute OnClick handler” 并开始执行队列中的下一个项目。
  • 注意:由于我们已经完成了计算,因此用户已经过了 3 分钟。 重新抽奖活动还没有发生!
  • 事件:使用 “计算” 值重新绘制状态 DIV。我们重新绘制并将其从队列中取出。
  • 事件:使用结果值重新绘制结果 DIV。我们重新绘制并将其从队列中取出。
  • 事件:使用 “完成” 值重新绘制状态 DIV。我们重新绘制并将其从队列中取出。眼尖的观众甚至可能会注意到 “状态 DIV 与” 计算 “值闪烁一分之一微秒 - 计算完成后

因此,潜在的问题是 “状态”DIV 的重新绘制事件在结束时被放置在队列中,在 “执行第 2 行” 事件之后需要 3 分钟,因此实际的重新绘制直到计算完成后。


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

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

  1. 将状态 “计算... 可能需要约 3 分钟” 填充到状态 DIV 中

  2. 使用 0 超时执行setTimeout()并调用LongCalc()函数

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

那么,事件序列和队列现在看起来像什么?

  • 队列: [Empty]
  • 事件:单击按钮。事件后的队列: [Execute OnClick handler(status update, setTimeout() call)]
  • 事件:在 OnClick 处理程序中执行第一行(例如,更改 Status 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 定时器如何工作的文章。设置超时时,它实际上会将异步代码排队,直到引擎执行当前调用堆栈。