ChatGPT解决这个技术问题 Extra ChatGPT

为什么 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() “稍后”调用我的函数可以避免什么浏览器问题?

大多数问题都描述了为什么它有用。如果您需要知道为什么会发生这种情况 - 请阅读我的回答:stackoverflow.com/a/23747597/1090562
菲利普·罗伯茨在他的演讲“事件循环到底是什么?”中以最好的方式解释了这一点。 youtube.com/watch?v=8aGhZQkoFbQ
如果您赶时间,这是他开始准确解决问题的视频部分:youtu.be/8aGhZQkoFbQ?t=14m54s。不管怎样,整个视频肯定值得一看。
setTimeout(fn)setTimeout(fn, 0) 相同,顺便说一句。
与这个问题相关的是the queueMicrotask() method,稍后介绍。

s
staticsan

在问题中,存在一个 race condition

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

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

之所以存在这种竞争,是因为 JavaScript 有一个与页面呈现共享的 single thread of execution。实际上,运行 JavaScript 会阻止 DOM 的更新。

您的解决方法是:

setTimeout(callback, 0)

Invoking setTimeout with 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执行不忙。

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

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

有关更详尽的解释,请参阅 Philip Roberts 的谈话 "What the heck is the event loop?"


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

前言:

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

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

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

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

细节:

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

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

进行很长的计算(比如需要 3 分钟) 将计算结果打印到结果 div 中。

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

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

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

将状态“正在计算...可能需要约 3 分钟”填充到状态 DIV 进行非常长的计算(例如需要 3 分钟)将计算结果打印到结果 div 中。将状态“计算完成”填充到状态 DIV

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

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

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

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

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

队列:[空]

事件:点击按钮。事件后排队:[执行 OnClick 处理程序(第 1-4 行)]

事件:在 OnClick 处理程序中执行第一行(例如更改状态 DIV 值)。事件后排队:[执行 OnClick 处理程序(第 2-4 行),使用新的“计算”值重新绘制状态 DIV]。请注意,虽然 DOM 更改是即时发生的,但要重新绘制相应的 DOM 元素,您需要一个由 DOM 更改触发的新事件,该事件位于队列末尾。

问题!!!问题!!!详细说明如下。

事件:在处理程序中执行第二行(计算)。队列之后:[执行 OnClick 处理程序(第 3-4 行),使用“正在计算”值重新绘制状态 DIV]。

事件:在处理程序中执行第 3 行(填充结果 DIV)。队列之后:[执行 OnClick 处理程序(第 4 行),使用“计算”值重新绘制状态 DIV,使用结果重新绘制结果 DIV]。

事件:在处理程序中执行第 4 行(使用“DONE”填充状态 DIV)。队列:[执行 OnClick 处理程序,使用“正在计算”值重新绘制状态 DIV,使用结果重新绘制结果 DIV;用“DONE”值重新绘制状态 DIV]。

事件:执行来自 onclick 处理程序子的隐含返回。我们从队列中取出“Execute OnClick 处理程序”并开始执行队列中的下一项。

注意:由于我们已经完成了计算,因此用户已经过了 3 分钟。重画事件还没有发生!!!

事件:使用“计算”值重新绘制状态 DIV。我们重新绘制并将其从队列中移除。

事件:使用结果值重新绘制 Result DIV。我们重新绘制并将其从队列中移除。

事件:使用“完成”值重新绘制状态 DIV。我们重新绘制并将其从队列中移除。眼尖的观众甚至可能会注意到“状态 DIV 与“正在计算”值在几分之一微秒内闪烁 - 计算完成后

所以,根本的问题是“状态”DIV的重绘事件被放置在最后的队列中,在“执行第2行”事件之后需要3分钟,所以实际的重绘不会发生,直到计算完成后。

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

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

将状态“正在计算...可能需要约 3 分钟”填充到状态 DIV 执行 setTimeout(),超时为 0 并调用 LongCalc() 函数。 LongCalc() 函数与上次几乎相同,但显然没有“正在计算...”状态 DIV 更新作为第一步;而是立即开始计算。

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

队列:[空]

事件:点击按钮。事件后排队:[执行 OnClick 处理程序(状态更新,setTimeout() 调用)]

事件:在 OnClick 处理程序中执行第一行(例如更改状态 DIV 值)。事件后排队:[执行 OnClick 处理程序(这是一个 setTimeout 调用),使用新的“计算”值重新绘制状态 DIV]。

事件:在处理程序中执行第二行(setTimeout 调用)。排队之后:[用“计算”值重新绘制状态 DIV]。队列在 0 秒内没有任何新内容。

事件:超时警报响起,0 秒后。排队之后:[用“计算”值重新绘制状态 DIV,执行 LongCalc(第 1-3 行)]。

事件:使用“计算”值重新绘制状态 DIV。排队之后:[执行 LongCalc(第 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);
});

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

查看 John Resig 关于 How JavaScript Timers Work 的文章。当您设置超时时,它实际上将异步代码排队,直到引擎执行当前调用堆栈。


J
Jose Basilio

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

看看这个:setTimeout


1
14 revs

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


“每个 JavaScript 执行和 UI 更新任务都被添加到浏览器事件队列系统中,然后这些任务被分派到浏览器主 UI 线程来执行。”....来源好吗?
High Performance JavaScript(Nicholas Zakas、Stoyan Stefanov、Ross Harmes、Julien Lecomte 和 Matt Sweeney)
对此 add this fn to the end of the queue 投反对票。最重要的是 setTimeout 在哪里添加了这个函数,这个循环周期的结束或下一个循环周期的开始。
V
Vladimir Kornea

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

“这就是为什么:不可能有 0 毫秒的时间延迟的 setTimeout。最小值由浏览器确定,它不是 0 毫秒。历史上浏览器将此最小值设置为 10 毫秒,但 HTML5 规范和现代浏览器将其设置为 4 毫秒。”

4 毫秒的最小超时与正在发生的事情无关。真正发生的是 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

你的回答并不能证明一件事。它只是表明在您的机器上在特定情况下计算机会向您抛出一些数字。要证明相关的东西,您需要的不仅仅是几行代码和几个数字。
@SalvadorDali,我相信我的证明足够清楚,大多数人都能理解。我认为你感到防御,并没有努力去理解它。我很乐意尝试澄清它,但我不知道你没有理解什么。如果您怀疑我的结果,请尝试在您自己的机器上运行代码。
P
Pointy

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

编辑 现在是 2015 年,我应该注意到还有 requestAnimationFrame(),虽然不完全相同,但它与 setTimeout(fn, 0) 足够接近,值得一提。


这正是我看到它被使用的地方之一。 =)
仅供参考:这个答案是从 stackoverflow.com/questions/4574940/… 合并到这里的
是将代码的执行推迟到一个单独的后续事件循环:你如何计算出一个后续事件循环?你怎么知道什么是当前的事件循环?你怎么知道你现在在哪个事件循环中?
@Green 好吧,你没有,真的;对于 JavaScript 运行时的工作,真的没有直接的可见性。
requestAnimationFrame 解决了我使用 IE 和 Firefox 有时不更新 UI 的问题
D
DanielSmedegaardBuus

这两个评价最高的答案都是错误的。 Check out the MDN description on the concurrency model and the event loop,并且应该清楚发生了什么(MDN 资源是一个真正的宝石)。除了“解决”这个小问题之外,简单地使用 setTimeout 可能会在您的代码中添加意想不到的问题。

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

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

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

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

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

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

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

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

在当前执行的帧完成(函数返回)之前,浏览器 UI 不能(或选择不)更新 UI。就个人而言,我认为这是设计而不是限制。

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

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

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

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

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

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

编写依赖于其逻辑的新事件处理程序时的“修复”是使用 setTimeout 0。但是,这不是解决办法,很难理解,而且调试由这样的代码引起的错误也没有乐趣。有时从来没有问题,有时它总是失败,然后又一次,有时它会工作并偶尔中断,这取决于平台的当前性能以及当时发生的其他事情。这就是为什么我个人建议不要使用这种 hack(它一种 hack,我们都应该知道它是),除非你真的知道自己在做什么以及后果是什么。

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

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


似乎是整个页面上唯一有效且完整的评论
S
Salvador Dali

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

所以你有两个功能:

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

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

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

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

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

同样来自Mozilla:

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

PS信息是在阅读以下article后获取的。


@user2407309 你在开玩笑吗?你的意思是HTML5规范是错误的,你是正确的?在您投反对票并提出强有力的主张之前,请阅读资源。我的回答是基于 HTML 规范和历史记录。我没有做一遍又一遍地解释完全相同的东西的答案,而是添加了一些新的东西,这些东西在以前的答案中没有显示。我并不是说这是唯一的原因,我只是在展示一些新的东西。
这是不正确的:“原因如下: setTimeout 不可能具有 0 毫秒的时间延迟。”这不是为什么。 4ms 的延迟与 setTimeout(fn,0) 有用的原因无关。
@user2407309 它可以很容易地修改为“添加到其他人陈述的原因,这是不可能的......”。因此,仅仅因为这个而投反对票是荒谬的,特别是如果你自己的答案没有告诉任何新的东西。只需一个小编辑就足够了。
Salvador Dali:如果你在这里忽略了微火焰战争的情感方面,你可能不得不承认@VladimirKornea 是对的。确实,浏览器将 0 毫秒延迟映射到 4 毫秒,但即使他们没有,结果仍然是相同的。这里的驱动机制是代码被推入队列,而不是调用堆栈。看看这个优秀的 JSConf 演示,它可能有助于澄清问题:youtube.com/watch?v=8aGhZQkoFbQ
我很困惑为什么您认为您的合格最低 4 毫秒报价是全球最低 4 毫秒。正如您对 HTML5 规范的引用所示,仅当您嵌套对 setTimeout/setInterval 的调用超过五级时,最短为 4 毫秒;如果你没有,最小值是 0 毫秒(缺少时间机器的情况下)。 Mozilla 的文档将其扩展到涵盖重复的情况,而不仅仅是嵌套情况(因此间隔为 0 的 setInterval 将立即重新安排几次,然后延迟更长的时间),但允许使用具有最少嵌套的 setTimeout 立即排队.
a
aderchox

如果您不想看a whole video,这里简单解释一下需要了解的内容,以便能够理解此问题的答案:

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

从现在开始,我们将讨论“在浏览器中”的 JavaScript。 setTimeout 之类的东西确实是浏览器的东西,而不是 JavaScript 本身的一部分。

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

现在让我们称另一个空间为第二个空间。

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

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

https://i.stack.imgur.com/ZBd8y.png

浏览器会跟踪 5 秒的延迟,一旦超过,它会查看主空间,“当它为空时”,将 fn(); 回调放在它上面。这就是 setTimeout 的工作原理。

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

我真的建议您也观看该视频,因为他的解释非常好,并且更多地打开了技术问题。


u
user113716

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


仅供参考:这个答案是从 stackoverflow.com/questions/4574940/… 合并到这里的
J
Jason Suárez

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


对此 push the function invocation to the bottom of the stack 投反对票。你在说什么 stack 是晦涩难懂的。最重要的是 setTimeout 在哪里添加了这个函数,这个循环周期的结束或下一个循环周期的开始。
J
Jeremy

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


f
fabspro

setTimeout 有用的其他一些情况:

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

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


在 onsubmit 事件中禁用会更好;它会更快,并且保证在技术上提交表单之前被调用,因为您可以停止提交。
非常真实。我认为 onclick 禁用更容易进行原型设计,因为您可以简单地在按钮中键入 onclick="this.disabled=true" 而在提交时禁用则需要更多的工作。
W
Willem van der Veen

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

setTimeout() 导致事件是异步的,因此在所有同步代码之后执行,从而为您的元素提供更多加载时间。像 setTimeout() 中的回调这样的异步回调被放入事件队列中,并在同步代码的堆栈为空后由事件循环放入堆栈。作为函数 setTimeout() 中第二个参数的 ms 的值 0 通常略高(4-10ms 取决于浏览器)。执行 setTimeout() 回调所需的稍长的时间是由事件循环的“滴答”量(如果堆栈为空,滴答将回调推送到堆栈上)的数量引起的。由于性能和电池寿命的原因,事件循环中的滴答数被限制为每秒少于 1000 次的特定数量。


C
ChrisN

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

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

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


我对你的回答有点怀疑,但赞成它,因为它迫使我对浏览器标准进行一些额外的研究。在研究标准时,我会去我经常去的地方,MDN:developer.mozilla.org/en-US/docs/Web/API/window.setTimeout HTML5 规范说 4ms。它没有说明移动芯片的时钟限制。很难在谷歌上搜索信息来源来支持您的陈述。确实发现 Google 的 Dart 语言完全删除了 setTimeout 以支持 Timer 对象。
(...)因为许多移动浏览器由于时钟限制而无法注册小于 20 毫秒的超时(...)每个平台都由于其时钟而受到时间限制,并且没有平台能够在 0ms 之后执行下一件事当前一个。 0ms 的超时要求尽快执行功能,特定平台的时间限制不会以任何方式改变其含义。
S
Stephan G

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

A
AnonymousUser

//当需要“new a”时,setTimeout(fn, 0) 很有用,当需要等待一些动作时。示例: var a = function (){console.log('a');}; var b = function(){setTimeout(b, 100);}; //在覆盖这个函数之前等待一些动作 //没有 setTimeout: console.log('no setTimeout: b.toString():', b.toString()); b(); //"b" 是旧函数 console.log('no setTieout: a.toString(): ', a.toString());一个(); //并且“a”没有被覆盖 setTimeout(//但是用 setTimeout(fn, 0): function(){ console.log('After timeout 0, b.toString(): ', b.toString()); b(); //"b" 是一个新函数 console.log('After timeout 0, a.toString(): ', a.toString()); a(); //and "a" 被覆盖 } , 0 ); //覆盖未定义的 var "b" b = function (){ a = function(){console.log('new a');}; }


欢迎来到堆栈溢出。没有任何解释的代码很少有帮助。 Stack Overflow 是关于学习的,而不是提供片段来盲目复制和粘贴。请编辑您的问题并解释它如何回答所提出的具体问题。请参阅How to Answer
此代码已注释,此注释包含答案。在setTimeout之前,函数a()没有被覆盖,在运行b()之后,但是在seTimeout之后,这个覆盖成功。
S
Sohail Yasmin

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