首页 文章

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

提问于
浏览
736

我最近碰到了一个相当讨厌的bug,其中代码是通过JavaScript动态加载 <select> . 这个动态加载的 <select> 具有预先选择的值 . 在IE6中,我们已经有了修复所选 <option> 的代码,因为有时候 <select>selectedIndex 值与选定的 <option>index 属性不同步,如下所示:

field.selectedIndex = element.index;

但是,这段代码没有正确设置,错误的索引最终会被选中 . 但是,如果我在正确的时间插入了 alert() 语句,则会选择正确的选项 . 考虑到这可能是某种时间问题,我尝试了一些随机的东西,我之前在代码中看到过:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

这有效!

我感到很不安,我不知道为什么这会解决我的问题 . 有人有官方解释吗?通过使用 setTimeout() 调用我的函数"later",我避免了哪些浏览器问题?

17 回答

  • 13

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

    现在编辑它's 2015 I should note that there'也是 requestAnimationFrame() ,这是't exactly the same but it'足够接近 setTimeout(fn, 0) ,值得一提 .

  • 21

    这是旧答案的旧问题 . 我想对此问题添加一个新的外观,并回答为什么会发生这种情况而不是为什么这有用 .

    所以你有两个功能:

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

    然后按以下顺序调用它们 f1(); f2(); 只是为了看到第一个执行的第一个 .

    这就是为什么: setTimeout 的时间延迟为0毫秒是不可能的 . Minimum value is determined by the browser 并且它不是0毫秒 . Historically浏览器将此最小值设置为10毫秒,但HTML5 specs和现代浏览器将其设置为4毫秒 .

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

    也来自mozilla:

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

    附:阅读以下article后获取信息 .

  • 9

    通过调用setTimeout,您可以给页面时间以响应用户正在执行的操作 . 这对于页面加载期间运行的函数特别有用 .

  • 8

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

  • 3

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

    这里存在相互冲突的褒奖答案,没有证据就没有办法知道相信谁 . 这是证明@DVK是正确的并且@SalvadorDali是错误的 . 后者声称:

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

    4ms的最小超时与发生的事情无关 . 真正发生的是setTimeout将回调函数推送到执行队列的末尾 . 如果在setTimeout(回调,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
    
  • 1

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

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

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

  • 0

    setTimeout有用的其他一些情况:

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

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

  • 2

    setTimeout() 购买了一些时间,直到加载DOM元素,即使设置为0 .

    看看这个:setTimeout

  • 17

    这两个都是评分最高的答案是错误的 . Check out the MDN description on the concurrency model and the event loop,它应该变得清楚发生了什么(MDN资源是一个真正的宝石) . 除了"solving"这个小问题之外, simply using setTimeout 可能会在您的代码中添加意外问题 .

    这里发生的 actually 不是"the browser might not be quite ready yet because concurrency,"或基于"each line is an event that gets added to the back of the queue"的东西 .

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

    's happening in his code is that he'首先将事件处理程序附加到 #do 按钮上的 click 事件 .

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

    这就是它变得有趣的地方 . 我们很容易忽视这个微不足道的事实: Any frame has to be executed, in full, before the next frame can be executed . 没有并发,人 .

    这是什么意思?这意味着无论何时从消息队列调用函数,它都会阻塞队列,直到它生成的堆栈被清空为止 . 或者,更一般地说,它会一直阻塞,直到函数返回 . 它会阻止 everything ,包括DOM渲染操作,滚动和诸如此类的东西 . 如果你想要确认,只是尝试增加小提琴中长时间运行操作的持续时间(例如,多次运行外循环10),并且你'll notice that while it runs, you cannot scroll the page. If it runs long enough, your browser will ask you if you want to kill the process, because it' s使页面无响应 . 正在执行框架,并且事件循环和消息队列将一直停留,直到完成为止 .

    那么为什么这个文本的副作用没有更新呢?因为当你 have 改变了DOM中元素的值时 - 你可以在更改它之后立即 console.log() 它的值并看到它已被更改(这表明为什么DVK 's explanation isn' t正确) - 浏览器正在等待堆栈耗尽(要返回的 on 处理函数)以及完成的消息,以便它最终能够执行运行时添加的消息作为对我们的变异操作的反应,并在UI中反映该变异 .

    这是因为我们实际上在等待代码完成运行 . 我们没有像我们通常使用基于事件的异步Javascript那样说"someone fetch this and then call this function with the results, thanks, and now I'm done so imma return, do whatever now," . 我们输入一个click事件处理函数,我们更新一个DOM元素,我们调用另一个函数,其他函数工作很长时间然后返回,然后我们更新相同的DOM元素,并且 then 我们从初始函数返回,有效地清空堆栈 . 并且 then 浏览器可以到达队列中的下一条消息,这可能是我们通过触发一些内部"on-DOM-mutation"类型事件生成的消息 .

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

    为什么 setTimeout 的东西呢?它这样做,因为它有效地从其自己的帧中删除对长时间运行的函数的调用,将其调度为稍后在 window 上下文中执行,以便它本身可以 return immediately 并允许消息队列处理其他消息 . 我们的想法是,在更改DOM中的文本时,我们在Javascript中触发的UI "on update"消息现在位于排队等待长时间运行的函数的消息之前,因此UI更新发生在我们阻塞很长时间之前时间 .

    请注意a)长时间运行的函数 still blocks 运行时的所有内容,以及b)您无法保证UI更新实际上在消息队列中位于其前面 . 在我的2018年6月的Chrome浏览器中, 0 的值并不是小提琴演示的问题--10确实如此 . 我在V8发动机中进行了一些可能会干扰的优化,或者我的理解可能就是缺乏 .

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

    首先,在这样的任何事件处理程序上使用 setTimeout 的问题,试图缓解另一个问题,很容易弄乱其他代码 . 这是我工作中的现实例子:

    一位同事在对事件循环的错误理解中,通过让一些模板渲染代码使用 setTimeout 0 进行渲染来尝试"thread" Javascript . 他不再在这里问,但我可以假设他可能会插入计时器来衡量渲染速度(这将是函数的返回即时性),并发现使用这种方法会使该函数的响应速度非常快 .

    第一个问题很明显;你不能使用javascript,所以你在添加混淆时不会赢得任何东西 . 其次,您现在已经从可能期望已经渲染了非常模板的可能事件侦听器的堆栈中有效地分离了模板的呈现,而它可能很少没有 . 该功能的实际行为现在是非确定性的 - 在不知情的情况下 - 任何运行它或依赖它的函数 . 您可以进行有根据的猜测,但无法正确编码其行为 .

    编写依赖于其逻辑的新事件处理程序时"fix"是 also 使用 setTimeout 0 . 但是,_283584都没有问题,有时候它总是失败,然后又有时它会偶尔起作用和破坏,这取决于平台当前的性能以及当时正在发生的其他事情 . 这就是为什么我个人会建议不要使用这个黑客(它是一个黑客,我们都应该知道它是这样),除非你真的知道你在做什么以及后果是什么 .

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

    哦,如果你在想,“好吧,我不能只在长时间运行的函数中调用它来使它异步吗?”然后没有 . 回调不会使它异步,它仍然必须在显式调用回调之前运行长时间运行的代码 .

  • 580

    大多数浏览器都有一个名为main thread的进程,它负责执行一些JavaScript任务,UI更新,例如:绘画,重绘或重排等 .

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

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

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

  • 702

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

  • 2

    Preface:

    重要提示:虽然它最受欢迎和接受,但@staticsan接受的答案实际上是 NOT CORRECT! - 请参阅David Mulder的解释原因 .

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

    因此,我发布了 detailed walk-through of what the browser does and how using setTimeout() helps . 它看起来很长,但实际上非常简单和直接 - 我只是非常详细 .

    UPDATE: 我已经制作了一个JSFiddle来演示以下解释:http://jsfiddle.net/C2YBE/31/ . 很多人都是@ThangChung帮助启动它 .

    UPDATE2: 为了防止JSFiddle网站死亡或删除代码,我在最后添加了代码到这个答案 .


    DETAILS

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

    "do something"按钮的 onClick 处理程序调用函数"LongCalc()",它执行以下两项操作:

    • 做了很长的计算(比如需要3分钟)

    • 将计算结果打印到结果div中 .

    现在,你的用户开始测试这个,单击“做某事”按钮,页面就在那里做3分钟看似没事,他们变得焦躁不安,再次点击按钮,等待1分钟,没有任何反应,再次点击按钮......

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


    所以你添加一个"Status" DIV(最初为空),并修改 onclick 处理程序(函数 LongCalc() )来做4件事:

    • 将状态“计算...可能需要约3分钟”填入状态DIV

    • 做了很长的计算(比如需要3分钟)

    • 将计算结果打印到结果div中 .

    • 将“已完成计算”状态填充到状态DIV中

    并且,您乐意将应用程序提供给用户重新测试 .

    他们回到你身边看起来很生气 . 并解释当他们点击按钮时, the Status DIV never got updated with "Calculating..." status!!!


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

    浏览器将事件产生的所有"TODO"任务(UI任务和JavaScript命令)放入 single queue . 不幸的是,使用新的"Calculating..."值重新绘制"Status" 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] . Please note that while the DOM changes happen instantaneously, to re-draw the corresponding DOM element you need a new event, triggered by the DOM change, that went at the end of the queue .

    • PROBLEM!!! PROBLEM!!! 详情如下 .

    • 事件:在处理程序(计算)中执行第二行 . 排队后: [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] .

    • 事件:从 onclick handler sub执行隐含的 return . 我们从队列中取出"Execute OnClick handler"并开始执行队列中的下一个项目 .

    • 注意:由于我们已经完成了计算,因此用户已经过了3分钟 . The re-draw event didn't happen yet!!!

    • 事件:使用"Calculating"值重新绘制状态DIV . 我们重新绘制并将其从队列中取出 .

    • 事件:使用结果值重新绘制结果DIV . 我们重新绘制并将其从队列中取出 .

    • 事件:使用"Done"值重新绘制状态DIV . 我们重新绘制并将其从队列中取出 . 眼尖的 Spectator 甚至可能会注意到"Status DIV with "计算“值闪烁一分之一微秒 - AFTER THE CALCULATION FINISHED

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


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

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

    • 将状态“计算...可能需要约3分钟”填入状态DIV

    • Execute setTimeout() with 0 timeout and a call to LongCalc() function .

    LongCalc() 功能与上次几乎相同,但显然没有"Calculating..."状态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)] .

    • 事件: re-draw Status DIV with "Calculating" value . 排队后: [execute LongCalc (lines 1-3)] . 请注意,此重新绘制事件可能实际发生在闹钟响起之前,这也适用 .

    • ......

    万岁!在计算开始之前,状态DIV刚刚更新为“计算...”!



    下面是来自JSFiddle的示例代码,说明了这些示例:http://jsfiddle.net/C2YBE/31/

    HTML code:

    <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 code: (Executed on onDomReady and may require 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);
    });
    
  • 0

    这是有效的,因为你正在进行合作多任务处理 .

    浏览器必须同时执行许多操作,其中只有一个是执行JavaScript . 但JavaScript经常用于的一件事是要求浏览器构建一个显示元素 . 这通常被认为是同步完成的(特别是当JavaScript不是并行执行时),但是不能保证这种情况并且JavaScript没有明确定义的等待机制 .

    解决方案是"pause" JavaScript执行让渲染线程赶上来 . 这是 setTimeout() 超时 0 的效果 . 它就像C中的一个线程/进程产量 . 虽然它似乎说"run this immediately"它实际上让浏览器有机会完成一些非JavaScript的事情,这些事情一直在等待完成这个新的JavaScript之前完成 .

    (实际上, setTimeout() 在执行队列的末尾重新排队新的JavaScript . 请参阅注释以获取更长解释的链接 . )

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


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

  • 1

    由于它被传递 0 的持续时间,我想它是为了从执行流中删除传递给 setTimeout 的代码 . 因此,如果它阻止后续代码执行 .

  • 20

    Javascript是单线程应用程序,因此不允许同时运行函数,因此要实现此事件循环 . 所以setTimeout(fn,0)正是这样做的,它被推入任务任务,当你的调用堆栈为空时执行 . 我知道这个解释很无聊,所以我建议你仔细阅读这个视频,这将有助于你在浏览器中如何工作 . 看看这个视频: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

  • 1

    问题是你试图对非现有元素执行Javascript操作 . 该元素尚未加载, setTimeout() 为以下列方式加载元素提供了更多时间:

    • setTimeout() 导致事件为 ansynchronous 因此在所有同步代码之后执行,从而为您的元素提供更多的加载时间 . 像 setTimeout() 中的回调一样的异步回调被放置在 event queue 中,并在 event loop 之后放入堆栈同步代码堆栈为空 .

    • ms作为函数 setTimeout() 中的第二个参数的值0通常略高(4-10ms,具体取决于浏览器) . 执行 setTimeout() 回调所需的稍高时间是由事件循环的'ticks'(如果堆栈为空,则滴答在堆栈上的回调)引起的 . 由于性能和电池寿命的原因,事件循环中的滴答数限制为每秒1000次以上 .

相关问题