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

2022-08-29 22:09:59

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

field.selectedIndex = element.index;

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

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

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

这奏效了!

我已经为我的问题找到了解决方案,但我感到不安的是,我不知道为什么这可以解决我的问题。有人有官方解释吗?我通过使用“稍后”调用我的函数来避免什么浏览器问题?setTimeout()


答案 1

在问题中,存在以下条件之间的争用条件

  1. 浏览器尝试初始化下拉列表,准备更新其选定的索引,以及
  2. 用于设置所选索引的代码

您的代码一直在赢得这场竞赛,并试图在浏览器准备就绪之前设置下拉选择,这意味着会出现错误。

之所以存在这种竞赛,是因为 JavaScript 具有与页面呈现共享的单一执行线程。实际上,运行 JavaScript 会阻止 DOM 的更新。

您的解决方法是:

setTimeout(callback, 0)

使用回调和零作为第二个参数进行调用将安排回调在尽可能短的延迟之后异步运行 - 当选项卡具有焦点并且执行的JavaScript线程不繁忙时,这将是大约10ms。setTimeout

因此,OP的解决方案是延迟约10ms,即所选索引的设置。这给了浏览器一个初始化DOM的机会,修复了这个错误。

每个版本的Internet Explorer都表现出古怪的行为,这种解决方法有时是必要的。或者,它可能是OP代码库中的真正错误。


请参阅菲利普·罗伯茨(Philip Roberts)的演讲“事件循环到底是什么?”以获取更全面的解释。


答案 2

前言:

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

因此,我正在发布浏览器功能以及使用setTimeout()如何帮助的详细演练。它看起来很长,但实际上非常简单明了 - 我只是把它做得非常详细。

更新:我做了一个JSFiddle来现场演示下面的解释:http://jsfiddle.net/C2YBE/31/非常感谢@ThangChung帮助启动它。

更新2:为了以防万一JSFiddle网站死亡或删除代码,我将代码添加到最后的答案中。


详细信息

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

“做某事”按钮的处理程序调用一个函数“LongCalc()”,它做2件事:onClick

  1. 进行很长的计算(假设需要3分钟)

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

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

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


因此,您添加一个“状态”DIV(最初为空),并修改处理程序(函数)以执行 4 项操作:onclickLongCalc()

  1. 填充状态“正在计算...可能需要大约 3 分钟“进入状态 DIV

  2. 进行很长的计算(假设需要3分钟)

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

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

而且,您很乐意将应用程序提供给用户进行重新测试。

他们回到你身边,看起来很生气。并解释说,当他们单击按钮时,状态DIV从未更新为“计算...”地位!!!


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

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

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

  • 队列:[Empty]
  • 事件:单击该按钮。事件后的队列:[Execute OnClick handler(lines 1-4)]
  • 事件:在 OnClick 处理程序中执行第一行(例如,更改状态 DIV 值)。事件后排队:。请注意,虽然 DOM 更改是即时发生的,但要重新绘制相应的 DOM 元素,您需要一个由 DOM 更改触发的新事件,该事件位于队列的末尾[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]
  • 问题!!!问题!!!详情如下所述。
  • 事件:执行处理程序中的第二行(计算)。队列之后:。[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • 事件:在处理程序中执行第三行(填充结果 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]
  • 事件:从处理程序子执行隐含。我们将“Execute OnClick处理程序”从队列中移除,并开始执行队列上的下一个项目。returnonclick
  • 注意:由于我们已经完成了计算,因此用户已经过了3分钟。重新绘制事件尚未发生!!!
  • 事件:使用“正在计算”值重新绘制状态 DIV。我们重新绘制并将其从队列中删除。
  • 事件:使用结果值重新绘制结果 DIV。我们重新绘制并将其从队列中删除。
  • 事件:使用“完成”值重新绘制状态 DIV。我们重新绘制并将其从队列中删除。眼神敏锐的观众甚至可能会注意到“状态 DIV 与'正在计算'值闪烁了一微秒的几分之一 - 计算完成后

因此,潜在的问题是,“Status” DIV 的重新绘制事件位于“执行行 2”事件之后的队列中,这需要 3 分钟,因此在计算完成之前,实际的重新绘制不会发生。


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

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

  1. 填充状态“正在计算...可能需要大约 3 分钟“进入状态 DIV

  2. 执行 setTimeout() 与 0 超时和对 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 调用)。队列之后:。队列中没有任何新内容,再过 0 秒。[re-draw Status DIV with "Calculating" value]
  • 事件:超时警报在 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/

代码:

<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);
});