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

2020/09/19 20:31 · javascript ·  · 0评论

我最近遇到了一个令人讨厌的错误,该错误中的代码是<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()

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

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

您的代码始终在这场比赛中取胜,并在浏览器就绪之前尝试设置下拉菜单,这意味着该错误将出现。

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

您的解决方法是:

setTimeout(callback, 0)

调用setTimeout回调,并在第二个参数为零时将调度回调以异步方式运行,并在最短的延迟后进行-当制表符具有焦点且JavaScript线程执行不忙时,该延迟约为10ms。

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

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


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

前言:

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

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

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

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


详情

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

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

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

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

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

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


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

  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]
  • 事件:returnonclick处理程序子执行隐式执行我们将“ Execute OnClick处理程序”从队列中移开,然后开始执行队列中的下一项。
  • 注意:由于我们已经完成了计算,因此用户已经过去了3分钟。重画事件尚未发生!!!
  • 事件:使用“计算”​​值重新绘制状态DIV。我们进行重画并将其移出队列。
  • 事件:使用结果值重新绘制结果DIV。我们进行重画并将其移出队列。
  • 事件:使用“完成”值重新绘制状态DIV。我们进行重画并将其移出队列。眼神敏锐的观众甚至可能会注意到“状态DIV带有“计算”值的闪烁几分之一秒- 计算完成后

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


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

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

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

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

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

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

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

setTimeout() 即使将DOM元素设置为0,也会让您花一些时间直到DOM元素加载完毕。

检查一下:setTimeout

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

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

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

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

什么是真正回事是不是“浏览器可能没有完全准备好,因为并发性”,或基于什么“每一行是被添加到队列的后面的事件”。

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

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

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

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

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

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

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

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

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

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

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

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

一位对事件循环有误解的同事试图通过使用一些模板渲染代码setTimeout 0来渲染Java 脚本。他不再在这里问,但是我可以假设他插入了计时器来评估渲染速度(这将是函数的返回即时性),并发现使用这种方法可以使该函数快速响应。

第一个问题很明显;您无法线程化javascript,因此在添加混淆时,您在这里一无所获。其次,您现在已经有效地从可能的事件侦听器堆栈中分离了模板的呈现,这些事件侦听器可能希望已经呈现了非常好的模板,而实际上可能还没有。现在,该功能的实际行为是不确定的,就像在不知不觉中一样,任何会运行该功能或依赖该功能的功能都是不确定的。您可以进行有根据的猜测,但不能对其行为进行适当的编码。

编写依赖于其逻辑的新事件处理程序时的“解决方案” 也要使用setTimeout 0但是,这不是一个解决办法,很难理解,调试由此类代码引起的错误也没有意思。有时永远不会有问题,有时它总是会失败,然后又一次,有时它会工作并偶尔崩溃,这取决于平台的当前性能以及当时发生的任何其他情况。这就是为什么我本人会建议不要使用此hack(这 hack,我们都应该知道),除非您真的知道自己在做什么以及后果如何。

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

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

这是一个带有旧答案的旧问题。我想重新看待这个问题,并回答为什么会发生这种情况,而不是为什么这样做很有用。

因此,您有两个功能:

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

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

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

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

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

同样来自mozilla:

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

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

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

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

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

setTimeout有用的其他一些情况:

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

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

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

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

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

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

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

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

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

本文地址:http://javascript.askforanswer.com/weishenmesettimeoutfn0youshiyouyong.html
文章标签: ,   ,  
版权声明:本文为原创文章,版权归 javascript 所有,欢迎分享本文,转载请保留出处!

文件下载

老薛主机终身7折优惠码boke112

上一篇:
下一篇:

评论已关闭!