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


#1楼

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


#2楼

setTimeout有用的其他一些情况:

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

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


#3楼

大多数浏览器都有一个称为主线程的进程,该进程负责执行一些JavaScript任务,UI更新,例如:绘画,重绘或重排等。

一些JavaScript执行和UI更新任务被排队到浏览器消息队列中,然后被分派到浏览器主线程来执行。

当在主线程繁忙时生成UI更新时,任务将添加到消息队列中。

setTimeout(fn, 0); 将此fn添加到要执行的队列的末尾。 它计划在给定的时间后将任务添加到消息队列中。


#4楼

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

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

而且,不涉及DOM操作的长期运行的进程应立即发送给Web Worker,因为它们提供了真正的JavaScript多线程执行。


#5楼

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

因此,您有两个功能:

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

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

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

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

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

同样来自mozilla:

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

阅读以下文章后即获得PS信息。


#6楼

这里有一些相互矛盾的,被否决的答案,没有证据就无法知道该相信谁。 这证明@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

#7楼

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

#8楼

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


#9楼

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

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


#10楼

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


#11楼

前言:

重要说明:尽管最受支持和接受,但@staticsan接受的答案实际上并不正确! -请参阅David Mulder的评论以解释原因。

其他一些答案是正确的,但实际上并未说明要解决的问题是什么,因此我创建了此答案以提供详细说明。

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

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

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


详情

想象一个带有“执行某项操作”按钮和一个结果div的Web应用程序。

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

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

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

现在,您的用户开始对此进行测试,单击“执行某些操作”按钮,页面坐在那里似乎在3分钟内什么都没做,他们变得不安,再次单击该按钮,等待1分钟,什么也没有发生,再次单击按钮...

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


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

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

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

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

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

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

他们回来时看起来很生气。 并说明一下,当他们单击按钮时, 状态DIV从未更新为“正在计算...”状态!


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

浏览器将事件产生的所有“ TODO”任务(UI任务和JavaScript命令)都放在一个队列中 。 不幸的是,用新的“正在计算...”值重新绘制“状态” 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]
  • 事件:在处理程序中执行第三行(填充结果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 handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • 事件:从onclick处理程序子执行隐式return 。 我们将“ Execute OnClick处理程序”从队列中移开,然后开始执行队列中的下一项。
  • 注意:由于我们已经完成了计算,因此用户已经过去了3分钟。 重画事件尚未发生!!!
  • 事件:使用“计算”​​值重新绘制状态DIV。 我们进行重画并将其移出队列。
  • 事件:使用结果值重新绘制结果DIV。 我们进行重画并将其移出队列。
  • 事件:使用“完成”值重新绘制状态DIV。 我们进行重画并将其移出队列。 眼神敏锐的观众甚至可能会注意到“状态DIV和“正在计算”值的闪烁几分之一秒- 计算完成后

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


进行救援的是setTimeout() 。 它有什么帮助? 因为实际上是通过setTimeout调用长时间执行的代码,所以实际上创建了2个事件: setTimeout执行本身和(由于0超时)分别为要执行的代码提供了队列条目。

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

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

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

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

#12楼

Javascript是单线程应用程序,因此不允许同时运行功能,因此要使用此事件循环。 因此,正是setTimeout(fn,0)所做的事情使它被塞入任务请求中,该任务在您的调用堆栈为空时执行。 我知道这个解释很无聊,所以我建议您看这段视频,这将帮助您在浏览器中进行工作。 观看此视频: -https : //www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ


#13楼

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

实际上 ,这里发生的并不是说“由于并发,浏览器可能还没有准备好”,也不是基于“每一行都是一个添加到队列后面的事件”。

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

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

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

这就是它变得有趣的地方。 我们习惯于将Javascript视为异步的,因此我们容易忽略了这个小小的事实: 在执行下一帧之前,必须完整执行任何框架 。 没有并发的人。

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

那么,为什么文本的这种副作用没有更新? 因为尽管你已经改变的DOM元素的值-你可以console.log()立即改变它后的价值,并认为它已经被改变(这说明了为什么DVK的解释是不正确的) -浏览器正在等待为了使堆栈耗尽(返回on handler函数)并因此完成消息,以便最终可以执行由运行时添加的消息,以响应我们的变异操作,从而在用户界面中反映出这种变化。

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

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

为什么setTimeout起作用呢? 这样做是因为它有效地从其自身的框架中删除了对长时间运行的函数的调用,将其调度为稍后在window上下文中执行,因此它本身可以立即返回并允许消息队列处理其他消息。 想法是,在更改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 worker与您的网页并列,并在完成计算后返回结果。

哦,如果您在想,“好吧,我不能只在长时间运行的函数中放一个回调以使其异步吗?”,然后否。 回调并不会使其异步,它仍必须在明确调用回调之前运行长时间运行的代码。


#14楼

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

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

#15楼

setTimeout()会花一些时间直到DOM元素加载,即使将其设置为0也是如此。

检查一下: setTimeout


#16楼

通过调用setTimeout,您可以给页面时间以响应用户所做的任何事情。 这对于页面加载期间运行的功能特别有用。


#17楼

之所以有效,是因为您正在执行协作式多任务处理。

浏览器几乎必须一次完成许多事情,其中​​之一就是执行JavaScript。 但是JavaScript经常使用的一件事是要求浏览器构建显示元素。 通常认为这是同步完成的(特别是因为JavaScript不是并行执行的),但不能保证是这种情况,并且JavaScript没有明确的等待机制。

解决方案是“暂停” JavaScript执行,以使渲染线程赶上来。 这就是setTimeout()的超时值为0的效果。 就像C中的线程/进程产量一样。尽管似乎说“立即运行”,但实际上,它使浏览器有机会完成一些非JavaScript的事情,而这些事情在加入此新的JavaScript之前一直在等待完成。 。

(实际上, setTimeout()在执行队列的末尾重新排队新的JavaScript。有关更多说明的链接,请参见注释。)

IE6恰好更容易出现此错误,但是我已经看到它发生在旧版本的Mozilla和Firefox中。


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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值