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

Why is setTimeout(fn, 0) sometimes useful?

提问人:Dan Lew 提问时间:4/23/2009 最后编辑:refactorDan Lew 更新时间:12/3/2021 访问量:388171

问:

我最近遇到了一个相当讨厌的错误,其中代码通过 JavaScript 动态加载。此动态加载具有预选值。在 IE6 中,我们已经有代码来修复 selected ,因为有时 的值会与 selected 的 属性不同步,如下所示:<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()

javascript dom 事件循环

评论

2赞 Salvador Dali 5/20/2014
大多数问题都描述了为什么它有用。如果您需要知道为什么会发生这种情况 - 请阅读我的回答:stackoverflow.com/a/23747597/1090562
32赞 vasa 5/14/2015
菲利普·罗伯茨(Philip Roberts)在他的演讲中以最好的方式解释了这一点:“事件循环到底是什么?youtube.com/watch?v=8aGhZQkoFbQ
2赞 Kyle Krzeski 6/10/2017
如果你赶时间,这是视频中他开始确切地解决问题的部分:youtu.be/8aGhZQkoFbQ?t=14m54s。Regarldless,整个视频肯定值得一看。
7赞 vsync 8/20/2018
setTimeout(fn)与 ,顺便说一句。setTimeout(fn, 0)
0赞 papo 8/21/2021
与这个问题相关的是 queueMicrotask() 方法,稍后会介绍它。

答:

29赞 Jose Basilio 4/23/2009 #1

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

看看这个: setTimeout

2赞 Jeremy 4/23/2009 #2

通过调用 setTimeout,您可以让页面有时间对用户正在执行的任何操作做出反应。这对于在页面加载期间运行的函数特别有用。

974赞 staticsan 4/23/2009 #3

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

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

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

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

您的解决方法是:

setTimeout(callback, 0)

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

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

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


请看菲利普·罗伯茨(Philip Roberts)的演讲“事件循环到底是什么?”,以获得更详尽的解释。

评论

316赞 David Mulder 6/4/2012
“解决方案是”暂停“JavaScript 执行,让渲染线程赶上来。”不完全正确,setTimeout 所做的是将一个新事件添加到浏览器事件队列中,并且渲染引擎已经在该队列中(不完全正确,但足够接近),因此它会在 setTimeout 事件之前执行。
54赞 staticsan 10/16/2013
是的,这是一个更详细、更正确的答案。但我的“足够正确”,让人们理解为什么这个伎俩有效。
2赞 jason 11/1/2013
@DavidMulder,这是否意味着浏览器解析 CSS 并在与 JavaScript 执行线程不同的线程中呈现?
8赞 David Mulder 11/5/2013
不,它们原则上是在同一线程中解析的,否则几行 DOM 操作会一直触发重排,这将对执行速度产生极其不利的影响。
32赞 davibq 11/10/2015
这个视频是为什么我们设置Timeout 0 2014.jsconf.eu/speakers/ 的最好解释......
93赞 Andy 12/7/2010 #4

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

17赞 Pointy 1/2/2011 #5

这样做的一个原因是将代码的执行推迟到一个单独的后续事件循环中。在响应某种类型的浏览器事件(例如鼠标单击)时,有时只有在处理当前事件才需要执行操作。该设施是最简单的方法。setTimeout()

现在是 2015 年,我应该注意到还有 ,这并不完全相同,但它非常接近,值得一提。requestAnimationFrame()setTimeout(fn, 0)

评论

0赞 Zaibot 1/3/2011
这正是我看到它被使用的地方之一。=)
0赞 Shog9 12/29/2013
仅供参考:这个答案是从 stackoverflow.com/questions/4574940/ 合并到这里的......
2赞 Green 10/22/2015
就是将代码的执行推迟到一个单独的后续事件循环中:你如何找出后续事件循环?你如何弄清楚什么是当前事件循环?你怎么知道你现在处于什么事件循环中?
1赞 Pointy 10/22/2015
@Green好吧,你没有,真的;实际上,无法直接了解 JavaScript 运行时在做什么。
0赞 David 1/22/2016
requestAnimationFrame 解决了我有时遇到 IE 和 Firefox 不更新 UI 的问题
8赞 user113716 1/2/2011 #6

由于它被传递了 的持续时间,我想这是为了从执行流程中删除传递给 的代码。因此,如果它是一个可能需要一段时间的函数,它不会阻止后续代码的执行。0setTimeout

评论

0赞 Shog9 12/29/2013
仅供参考:这个答案是从 stackoverflow.com/questions/4574940/ 合并到这里的......
725赞 DVK 1/2/2011 #7

前言:

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

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

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

UPDATE2:为了以防万一 JSFiddle 网站死机或删除代码,我在最后添加了代码。


细节

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

“do something”按钮的处理程序调用一个函数“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。value 是一个单独的 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]
  • 事件:在处理程序中执行第 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]
  • 事件:从处理程序 sub 执行隐含的执行。我们将“执行 OnClick 处理程序”从队列中移除,并开始执行队列中的下一项。returnonclick
  • 注意:由于我们已经完成了计算,因此用户已经过去了 3 分钟。重新绘制事件尚未发生!!
  • 事件:使用“正在计算”值重新绘制状态 DIV。我们进行重新绘制并将其从队列中移除。
  • 事件:使用结果值重新绘制结果 DIV。我们进行重新绘制并将其从队列中移除。
  • 事件:使用“完成”值重新绘制状态 DIV。我们进行重新绘制并将其从队列中移除。 眼尖的观众甚至可能会注意到“状态 DIV”的“正在计算”值闪烁了几分之一微秒 - 计算完成后

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


救援来了.它有什么帮助?因为通过调用长时间执行的代码,您实际上创建了 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/

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

评论

12赞 kumikoda 5/10/2013
很好的答案DVK!下面是一个要点,说明您的示例 gist.github.com/kumikoda/5552511#file-timeout-html
4赞 thangchung 12/11/2013
真的很酷的答案,DVK。为了便于想象,我把这些代码放到了jsfiddle jsfiddle.net/thangchung/LVAaV
4赞 DVK 12/24/2013
@ThangChung - 我试图在 JSFiddle 中制作一个更好的版本(2 个按钮,每种情况一个)。由于某种原因,它可以在 Chrome 和 IE 上作为演示,但在 FF 上不起作用 - 请参阅 jsfiddle.net/C2YBE/31。我问为什么FF在这里不起作用:stackoverflow.com/questions/20747591/......
1赞 bhavya_w 12/18/2014
@DVK “浏览器将事件生成的所有”TODO“任务(包括 UI 任务和 JavaScript 命令)放入单个队列中”。先生,您能提供这方面的来源吗?恕我直言,浏览器不应该有不同的 UI(渲染引擎)和 JS 线程......无意冒犯......只想学习..
1赞 Seba Kerckhof 2/20/2015
@bhavya_w 不,一切都发生在一个线程上。这就是长 js 计算会阻塞 UI 的原因
3赞 Jason Suárez 6/21/2012 #8

这样做的另一件事是将函数调用推送到堆栈的底部,从而防止在递归调用函数时堆栈溢出。这具有循环的效果,但允许 JavaScript 引擎触发其他异步计时器。while

评论

0赞 Green 5/29/2018
对此投反对票。你在说什么是晦涩难懂的。最重要的是,这个函数的确切位置,这个循环周期的结束或下一个循环周期的开始。push the function invocation to the bottom of the stackstacksetTimeout
2赞 fabspro 12/14/2012 #9

setTimeout 有用的其他一些情况:

您希望将长时间运行的循环或计算分解为更小的组件,以便浏览器不会出现“冻结”或说“页面上的脚本繁忙”。

您希望在单击时禁用表单提交按钮,但如果在 onClick 处理程序中禁用该按钮,则不会提交表单。时间为零的 setTimeout 可以解决问题,允许事件结束,表单开始提交,然后可以禁用您的按钮。

评论

2赞 Kris 1/6/2013
最好在 onsubmit 事件中禁用;它会更快,并且保证在技术上提交表单之前被调用,因为您可以停止提交。
0赞 fabspro 2/27/2014
非常正确。我想 onclick 禁用对于原型设计来说更容易,因为您只需在按钮中键入 onclick=“this.disabled=true”,而在提交时禁用需要稍多的工作。
26赞 14 revsArley #10

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

评论

0赞 bhavya_w 12/18/2014
“每个 JavaScript 执行和 UI 更新任务都会添加到浏览器事件队列系统中,然后这些任务被调度到浏览器主 UI 线程来执行。”....请问来源?
4赞 Arley 12/24/2014
高性能 JavaScript(Nicholas Zakas、Stoyan Stefanov、Ross Harmes、Julien Lecomte 和 Matt Sweeney)
0赞 Green 5/29/2018
对此投反对票。最重要的是,这个函数的确切位置,这个循环周期的结束或下一个循环周期的开始。add this fn to the end of the queuesetTimeout
1赞 ChrisN 3/14/2013 #11

关于执行循环和在其他一些代码完成之前呈现 DOM 的答案是正确的。JavaScript 中的零秒超时有助于使代码成为伪多线程,即使它不是。

我想补充一点,JavaScript 中跨浏览器/跨平台零秒超时的最佳值实际上是大约 20 毫秒而不是 0(零),因为由于 AMD 芯片的时钟限制,许多移动浏览器无法注册小于 20 毫秒的超时。

此外,不涉及 DOM 操作的长时间运行的进程现在应该发送给 Web Worker,因为它们提供了真正的 JavaScript 多线程执行。

评论

2赞 John Zabroski 5/19/2013
我对你的回答有点怀疑,但赞成它,因为它迫使我对浏览器标准做一些额外的研究。在研究标准时,我会去我经常去的地方,MDN:developer.mozilla.org/en-US/docs/Web/API/window.setTimeout HTML5 规范说 4ms。它没有说明移动芯片的时钟限制。很难在谷歌上搜索信息来源来支持您的陈述。确实发现 Google 的 Dart Language 完全删除了 setTimeout 以支持 Timer 对象。
8赞 Piotr Dobrogost 5/31/2013
(...) 因为由于时钟限制,许多移动浏览器无法注册小于 20 毫秒的超时 (...)由于时钟的原因,每个平台都有时序限制,没有一个平台能够在当前操作后 0 毫秒内执行下一件事。0ms 的超时要求尽快执行函数,特定平台的时序限制不会以任何方式改变其含义。
13赞 Salvador Dali 5/20/2014 #12

这是一个老问题,老答案。我想对这个问题进行新的审视,并回答为什么会发生这种情况,而不是为什么这有用。

所以你有两个函数:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

然后按以下顺序调用它们,只是为了查看第二个首先执行。f1(); f2();

原因如下:不可能有 0 毫秒的时间延迟。最小值由浏览器确定,不是 0 毫秒。从历史上看,浏览器将此最小值设置为 10 毫秒,但 HTML5 规范和现代浏览器将其设置为 4 毫秒。setTimeout

如果嵌套级别大于 5,且超时小于 4,则 将超时增加到 4。

同样来自mozilla:

若要在现代浏览器中实现 0 毫秒超时,可以使用 window.postMessage(),如下所述

P.S. 信息是在阅读以下文章后获取的。

评论

1赞 Salvador Dali 10/9/2014
@user2407309你在开玩笑吗?你的意思是 HTML5 规范是错误的,而你是对的?在投反对票并提出强有力的主张之前,请阅读消息来源。我的答案是基于 HTML 规范和历史记录。我没有一遍又一遍地解释完全相同的东西,而是添加了一些新的东西,一些在以前的答案中没有显示的东西。我并不是说这是唯一的原因,我只是在展示一些新的东西。
1赞 Val Kornea 10/9/2014
这是不正确的:“原因如下:不可能有 setTimeout 的时间延迟为 0 毫秒。这不是原因。4ms的延迟与为什么有用无关。setTimeout(fn,0)
0赞 Salvador Dali 10/9/2014
@user2407309可以很容易地将其修改为“为了增加其他人陈述的理由,这是不可能的......”。因此,仅仅因为这一点而投反对票是荒谬的,特别是如果你自己的答案没有说出任何新的东西。只需进行小编辑就足够了。
12赞 hashchange 7/23/2015
萨尔瓦多·达利:如果你忽略这里微火焰战争的情感方面,你可能不得不承认@VladimirKornea是对的。确实,浏览器将 0 毫秒的延迟映射到 4 毫秒,但即使它们没有,结果仍然是一样的。这里的驱动机制是将代码推送到队列上,而不是调用堆栈上。看看这个优秀的 JSConf 演示文稿,它可能有助于澄清问题:youtube.com/watch?v=8aGhZQkoFbQ
2赞 ShadowRanger 9/28/2019
我很困惑为什么你认为你的报价在合格的最小 4 毫秒是全局最小 4 毫秒。正如您在 HTML5 规范中的引用所示,仅当您嵌套调用深度超过 5 个级别时,最小值为 4 毫秒;如果没有,最小值为 0 毫秒(缺少时间机器)。Mozilla的文档将其扩展为涵盖重复的,而不仅仅是嵌套的情况(因此间隔为0将立即重新安排几次,然后在此之后延迟更长的时间),但允许使用最少的嵌套的简单使用立即排队。setTimeoutsetIntervalsetIntervalsetTimeout
26赞 Val Kornea 7/26/2014 #13

这里有相互矛盾的赞成答案,没有证据,就没有办法知道该相信谁。这证明@DVK是对的,@SalvadorDali是不正确的。后者声称:

“原因如下:不可能有带有时间的 setTimeout 延迟 0 毫秒。最小值由 浏览器,它不是 0 毫秒。从历史上看,浏览器会设置这个 最小到 10 毫秒,但 HTML5 规范和现代浏览器 将其设置为 4 毫秒。

4ms 最小超时与正在发生的事情无关。实际情况是 setTimeout 将回调函数推送到执行队列的末尾。如果在 setTimeout(callback, 0) 之后,您的阻塞代码需要几秒钟才能运行,则在阻塞代码完成之前,回调将在几秒钟内不会执行。请尝试以下代码:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

输出为:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

评论

0赞 Salvador Dali 10/9/2014
你的答案并不能证明什么。它只是表明在您的机器上,在特定情况下,计算机会抛出一些数字。要证明相关的东西,你需要的不仅仅是几行代码和几个数字。
7赞 Val Kornea 10/9/2014
@SalvadorDali,我相信我的证据足够清楚,大多数人都能理解。我认为你感到防御,没有努力去理解它。我很乐意尝试澄清它,但我不知道你不明白什么。如果您怀疑我的结果,请尝试在您自己的计算机上运行代码。
1赞 Stephan G 9/2/2015 #14

setTimout on 0 在设置延迟承诺的模式中也非常有用,您希望立即返回:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
-2赞 Jani Devang 1/22/2018 #15

Javascript 是单线程应用程序,因此不允许同时运行函数,因此为了实现此事件循环,使用了循环。因此,setTimeout(fn, 0) 究竟做了什么,它变成了任务任务,当您的调用堆栈为空时,该任务将执行。我知道这个解释很无聊,所以我建议你看完这个视频,这将有助于你在浏览器中的工作原理。 请看这个视频:- https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

20赞 DanielSmedegaardBuus 6/5/2018 #16

这两个最受好评的答案都是错误的。查看关于并发模型和事件循环的 MDN 描述,应该会清楚发生了什么(MDN 资源是一个真正的宝石)。除了“解决”这个小问题之外,简单地使用就可以在代码中添加意想不到的问题。setTimeout

这里实际发生的事情并不是“浏览器可能还没有完全准备好,因为并发性”,或者基于“每一行都是一个添加到队列后面的事件”的东西。

DVK 提供的 jsfiddle 确实说明了一个问题,但他对此的解释是不正确的。

在他的代码中发生的情况是,他首先将事件处理程序附加到按钮上的事件。click#do

然后,当您实际单击该按钮时,将创建一个引用事件处理程序函数,该函数被添加到 .当 到达此消息时,它会在堆栈上创建一个,并在 jsfiddle 中调用 click 事件处理程序的函数。messagemessage queueevent loopframe

这就是它变得有趣的地方。我们习惯于认为 Javascript 是异步的,以至于我们很容易忽略这个小事实:任何帧都必须完整执行,然后才能执行下一帧。没有并发,人。

这是什么意思?这意味着,每当从消息队列调用函数时,它都会阻塞队列,直到它生成的堆栈被清空。或者,更一般地说,它会阻塞,直到函数返回。它阻止了一切,包括 DOM 渲染操作、滚动等等。如果要确认,只需尝试增加小提琴中长时间运行操作的持续时间(例如,再运行外循环 10 次),您会注意到,当它运行时,您无法滚动页面。如果它运行的时间足够长,您的浏览器会询问您是否要终止该进程,因为它会使页面无响应。帧正在执行,事件循环和消息队列将停滞,直到它完成。

那么,为什么文本的这种副作用没有更新呢?因为当你在 DOM 中更改了元素的值时——你可以在更改它后立即查看它的值,并看到它已被更改(这显示了为什么 DVK 的解释不正确)——浏览器正在等待堆栈耗尽(处理函数返回)并因此完成消息,以便它最终可以执行运行时添加的消息作为对我们的突变操作做出反应,并为了在UI中反映该突变。console.log()on

这是因为我们实际上是在等待代码完成运行。我们没有说过“有人获取这个,然后调用这个函数并得到结果,谢谢,现在我已经完成了,所以imma return,现在做任何事情”,就像我们通常对基于事件的异步 Javascript 所做的那样。我们进入一个点击事件处理函数,我们更新一个 DOM 元素,我们调用另一个函数,另一个函数工作很长时间然后返回,然后我们更新同一个 DOM 元素,然后我们从初始函数返回,有效地清空堆栈。然后浏览器可以访问队列中的下一条消息,这很可能是我们通过触发一些内部“on-DOM-mutation”类型事件生成的消息。

浏览器 UI 无法(或选择不)更新 UI,直到当前执行的帧完成(函数已返回)。就我个人而言,我认为这与其说是限制,不如说是设计使然。

那为什么这件事会起作用呢?它之所以这样做,是因为它有效地从自己的帧中删除了对长时间运行的函数的调用,将其安排为稍后在上下文中执行,以便它本身可以立即返回并允许消息队列处理其他消息。这个想法是,我们在 Javascript 中更改 DOM 中的文本时触发的 UI“on update”消息现在位于为长时间运行的函数排队的消息之前,因此 UI 更新发生在我们阻止很长时间之前。setTimeoutwindow

请注意,a) 长时间运行的函数在运行时仍会阻止所有内容,并且 b) 您不能保证 UI 更新实际上在消息队列中领先于它。在我 2018 年 6 月的 Chrome 浏览器上,值 10 并不能“修复”小提琴演示的问题。我实际上对此有点窒息,因为在我看来,UI 更新消息应该在它之前排队似乎是合乎逻辑的,因为它的触发器是在计划长时间运行的函数“稍后”运行之前执行的。但也许 V8 发动机中有一些优化可能会干扰,或者我的理解只是缺乏。0

好的,那么使用 有什么问题,对于这种特殊情况,有什么更好的解决方案?setTimeout

首先,在这样的事件处理程序上使用,试图缓解另一个问题,很容易与其他代码混淆。这是我工作中的一个真实例子:setTimeout

一位同事对事件循环的理解是错误的,他试图通过使用一些模板渲染代码来“线程”Javascript。他不再在这里问了,但我可以推测,也许他插入了计时器来衡量渲染速度(这将是函数的返回即时性),并发现使用这种方法可以使该函数的响应速度极快。setTimeout 0

第一个问题很明显;你不能线程 JavaScript,所以当你添加混淆时,你在这里什么也赢不了。其次,您现在已经有效地将模板的呈现与可能的事件侦听器堆栈分离,这些事件侦听器可能期望该模板已被呈现,而很可能没有呈现。该函数的实际行为现在是非确定性的,就像任何运行它或依赖它的函数一样——在不知不觉中如此。您可以做出有根据的猜测,但无法正确编码其行为。

在编写依赖于其逻辑的新事件处理程序时,“解决”使用 .但是,这不是一个修复程序,很难理解,调试由此类代码引起的错误并不好玩。有时永远没有问题,有时它偶尔会失败,然后又会偶尔工作和中断,这取决于平台的当前性能以及当时发生的任何其他事情。这就是为什么我个人建议不要使用这个黑客(这是一个黑客,我们都应该知道它是),除非你真的知道你在做什么以及后果是什么。setTimeout 0

但是我们做些什么呢?好吧,正如引用的 MDN 文章所建议的那样,要么将工作拆分为多条消息(如果可以的话),以便排队的其他消息可以与您的工作交错并在运行时执行,或者使用 Web Worker,它可以与您的页面一起运行,并在完成计算后返回结果。

哦,如果你在想,“好吧,我不能在长期运行的函数中放一个回调来使其异步吗?”那么不行。回调不会使其异步,在显式调用回调之前,它仍然必须运行长时间运行的代码。

评论

3赞 rottweilers_anonymous 7/17/2020
似乎是整个页面上唯一有效且完整的评论
1赞 Noob 1/9/2023
解释得很好,谢谢!
0赞 Moritz Friedrich 12/4/2023
根据我的理解,这不适用于其他浏览器组件执行的操作,例如网络请求、DOM 渲染或文件系统访问,对吧?也就是说,实际上将由另一个线程运行的东西,与 Javascript 执行无关。
2赞 Willem van der Veen 9/15/2018 #17

问题是您尝试对不存在的元素执行 Javascript 操作。该元素尚未加载,并按以下方式为元素提供了更多时间加载:setTimeout()

  1. setTimeout()使事件是异步的,因此在所有同步代码之后执行,从而为元素提供更多加载时间。异步回调(如回调)放置在事件队列中,并在同步代码堆栈为空后由事件循环放在堆栈上。setTimeout()
  2. 作为函数中第二个参数的 ms 值 0 通常略高(4-10ms,具体取决于浏览器)。执行回调所需的时间略高是由事件循环的“ticks”(如果堆栈为空,则tick在堆栈上推送回调)的数量引起的。由于性能和电池寿命的原因,事件循环中的滴答量被限制在一定数量内,低于每秒 1000 次。setTimeout()setTimeout()
21赞 aderchox 5/23/2021 #18

如果你不想看一个完整的视频,这里有一个简单的解释,需要理解的事情,以便能够理解这个问题的答案:

  1. JavaScript 是单线程的,这意味着它在运行时一次只做一件事。
  2. 但是 JavaScript 运行的环境可以是多线程的。例如,浏览器通常是多线程生物,即能够一次做很多事情。因此,他们可以运行 JavaScript,同时跟踪处理其他东西。

从现在开始,我们谈论的是“浏览器中的”JavaScript。像这样的东西确实是浏览器的东西,而不是 JavaScript 本身的一部分。setTimeout

  1. 允许 JavaScript 异步运行的是多线程浏览器!除了 Javascript 使用的主要空间(称为调用堆栈)来放置每行代码并逐一运行它们之外,浏览器还为 JavaScript 提供了另一个空间来放置内容。

现在,我们把另一个空间称为第二个空间

  1. 我们假设是一个函数。这里要理解的重要一点是,fn(); call 不等于 setTimeout(fn, 0); call,下面将进一步解释。fn

让我们先假设另一个延迟,而不是延迟,例如,5000 毫秒:。需要注意的是,这仍然是一个“函数调用”,所以它必须放在主空间上,并在完成后从主空间中删除,但是等等!,我们不喜欢整个冗长而无聊的 5 秒延迟。这将阻塞主空间,并且不允许 JavaScript 在此期间运行任何其他内容。0setTimeout(fn, 5000);

值得庆幸的是,这不是浏览器设计者设计它们的方式。相反,此调用(setTimeout(fn, 5000);)立即完成的。这一点非常重要:即使有 5000 毫秒的延迟,这个函数调用也会在瞬间完成!接下来会发生什么?它从主空间中删除。它会放在哪里?(因为我们不想失去它)。您可能猜对了:浏览器听到此调用并将其放在第二个空格上。 enter image description here

浏览器会跟踪 5 秒的延迟,一旦通过,它就会查看主空间,并且“当它为空时”,将 fn(); 回调。这就是工作方式。setTimeout

所以,回到 ,即使延迟为零,这仍然是对浏览器的调用,浏览器立即听到并拾取它,并将其放在第二个空间上,只有在主空间再次为空时才将其放回主空间,而不是真正的 0 毫秒后setTimeout(fn, 0)

我真的建议你也看这个视频,因为他解释得非常好,而且对技术问题有更多了解。

评论

0赞 Monday Fatigue 3/10/2022
html.spec.whatwg.org/multipage/......
0赞 AnonymousUser 7/12/2021 #19

//When need "new a", setTimeout(fn, 0) is useful, when need to wait some action. Example:

var a = function (){console.log('a');};
var b = function(){setTimeout(b, 100);}; //wait some action before override this function

//without setTimeout:
console.log('no setTimeout: b.toString():', b.toString());
b();    //"b" is an old function
console.log('no setTieout: a.toString(): ', a.toString());
a();    //and "a" is not overrided

setTimeout(//but with setTimeout(fn, 0):
    function(){
        console.log('After timeout 0, b.toString(): ', b.toString());
        b();    //"b" is a new function
        console.log('After timeout 0, a.toString(): ', a.toString());
        a();    //and "a" is overrided
    },
    0
);

//override var "b", which was been undefined
b = function (){
    a = function(){console.log('new a');};
}

评论

0赞 Sfili_81 7/12/2021
欢迎来到 Stack Overflow。没有任何解释的代码很少有帮助。Stack Overflow 是关于学习的,而不是提供盲目复制和粘贴的片段。请编辑您的问题并解释它如何回答所提出的特定问题。请参阅如何回答
0赞 AnonymousUser 7/13/2021
此代码已注释,此注释包含答案。在 setTimeout 之前,函数 a() 在运行 b() 后没有被覆盖,但在 seTimeout 之后,它被成功覆盖。