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

IT技术 javascript dom event-loop
2021-01-03 15:45:43

我最近遇到了一个相当讨厌的错误,其中代码是<select>通过 JavaScript 动态加载的这个动态加载<select>有一个预先选择的值。在 IE6 中,我们已经有了修复 selected 的代码<option>,因为有时<select>selectedIndex值会与 selected<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()

6个回答

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

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

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

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

您的解决方法是:

setTimeout(callback, 0)

Invoking setTimeoutwith a callback, and zero as the second argument will schedule the callback to be run asynchronously , after the shortest possible delay - which will be around 10ms when the tab has focus and the JavaScript thread of execution is not busy.

因此,OP 的解决方案是将所选索引的设置延迟约 10 毫秒。这为浏览器提供了初始化 DOM 的机会,从而修复了错误。

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


参见 Philip Roberts 的演讲“事件循环到底是什么?” 以获得更彻底的解释。

是的,这是一个更详细、更正确的答案。但我的“足够正确”让人们理解为什么这个技巧有效。
2021-02-09 15:45:43
@DavidMulder,这是否意味着浏览器解析 css 并在与 javascript 执行线程不同的线程中呈现?
2021-02-12 15:45:43
“解决方案是“暂停”JavaScript 执行,让渲染线程跟上。” 不完全正确,setTimeout 所做的是向浏览器事件队列添加一个新事件,并且渲染引擎已经在该队列中(不完全正确,但足够接近),因此它在 setTimeout 事件之前执行。
2021-02-22 15:45:43
不,它们原则上是在同一个线程中解析的,否则几行 DOM 操作就会一直触发回流,这将对执行速度产生极其不利的影响。
2021-02-24 15:45:43
这个视频是最好的解释为什么我们 setTimeout 0 2014.jsconf.eu/speakers/...
2021-02-25 15:45:43

前言:

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

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

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

UPDATE2:以防万一 JSFiddle 网站死亡,或删除代码,我在最后将代码添加到此答案中。


详情

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

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

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

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

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

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


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

  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 值)。事件后的队列:[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]
  • 事件:在处理程序中执行第三行(填充结果 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]
  • 事件:执行returnonclick处理程序子隐含我们从队列中取出“Execute OnClick 处理程序”并开始执行队列中的下一项。
  • 注意:由于我们已经完成了计算,对于用户来说已经过去了 3 分钟。重画事件还没有发生!!!
  • 事件:使用“正在计算”值重新绘制状态 DIV。我们重新绘制并将其从队列中移除。
  • 事件:使用结果值重新绘制结果 DIV。我们重新绘制并将其从队列中移除。
  • 事件:使用“完成”值重新绘制状态 DIV。我们重新绘制并将其从队列中移除。眼尖的观众甚至可能会注意到“正在计算”值闪烁的“状态 DIV” -计算完成后

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


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

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

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

  2. setTimeout()以 0 超时和对LongCalc()function的调用执行

    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 代码:(在onDomReadyjQuery 1.9上执行并且可能需要 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);
});
@bhavya_w 不,一切都发生在一个线程上。这就是长时间的 js 计算会阻塞 UI 的原因
2021-02-08 15:45:43
很好的答案 DVK!这是一个说明您的示例的要点gist.github.com/kumikoda/5552511#file-timeout-html
2021-02-09 15:45:43
非常酷的答案,DVK。为了便于想象,我已将该代码放到 jsfiddle jsfiddle.net/thangchung/LVAaV
2021-03-01 15:45:43
@ThangChung - 我试图在 JSFiddle 中制作一个更好的版本(2 个按钮,每个按钮一个)。它在 Chrome 和 IE 上用作演示,但由于某种原因不能在 FF 上运行 - 请参阅jsfiddle.net/C2YBE/31我问为什么 FF 在这里不起作用:stackoverflow.com/questions/20747591/...
2021-03-02 15:45:43
@DVK“浏览器将所有由事件产生的“TODO”任务(UI 任务和 JavaScript 命令)放入一个队列中”。楼主,能提供一下这个的出处吗?恕我直言,浏览器应该有不同的 UI(渲染引擎)和 JS 线程....无意冒犯..只是想学习..
2021-03-06 15:45:43

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

浏览器有一个叫做“主线程”的进程,它负责执行一些 JavaScript 任务,UI 更新,例如:绘画、重绘、重排等。 JavaScript 任务被排队到一个消息队列,然后被分派到浏览器的主线程进行执行。当主线程忙时生成 UI 更新时,将任务添加到消息队列中。

高性能 JavaScript(Nicholas Zakas、Stoyan Stefanov、Ross Harmes、Julien Lecomte 和 Matt Sweeney)
2021-02-22 15:45:43
“每个 JavaScript 执行和 UI 更新任务都被添加到浏览器事件队列系统中,然后这些任务被分派到浏览器主 UI 线程来执行。”....来源好吗?
2021-03-04 15:45:43
对此投反对票add this fn to the end of the queue最重要的是确切地setTimeout添加此 func 的位置、此循环循环的结束或下一个循环循环的开始。
2021-03-09 15:45:43

setTimeout() 即使设置为 0,也会在加载 DOM 元素之前为您争取一些时间。

看看这个:setTimeout