代码之家  ›  专栏  ›  技术社区  ›  Dan Lew

为什么设置超时(fn,0)有时有用?

  •  753
  • Dan Lew  · 技术社区  · 16 年前

    我最近遇到了一个相当讨厌的bug,其中代码正在加载一个 <select> 通过javascript动态。这是动态加载的 <选择& 有一个预先选择的值。在IE6中,我们已经有代码来修复选定的 <option> 因为有时候 <选择& selectedIndex 值将与所选的不同步 <选项& GT; 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() ?

    17 回复  |  直到 6 年前
        1
  •  719
  •   dev.e.loper    6 年前

    这是因为你在做合作性的多任务工作。

    一个浏览器必须同时做很多事情,其中之一就是执行javascript。但javascript经常使用的一个功能是要求浏览器构建一个显示元素。这通常被认为是同步执行的(尤其是当javascript不是并行执行时),但不能保证是这样,而且javascript没有定义好的等待机制。

    解决方案是“暂停”JavaScript的执行,让渲染线程跟上进度。这就是 setTimeout() 超时为 做。这就像是C中的线程/进程收益率。虽然它似乎说“立即运行这个”,但实际上它给了浏览器一个机会,让浏览器完成一些在处理这个新的JavaScript之前一直等待完成的非JavaScript操作。

    (实际上, 设置TimeTo() 在执行队列的末尾重新排队新的javascript。请参阅注释以获取指向较长解释的链接。)

    IE6恰好更容易出现这种错误,但我已经看到它出现在老版本的Mozilla和Firefox中。


    见菲利普罗伯茨谈话 "What the heck is the event loop?" 更详细的解释。

        2
  •  592
  •   Rajshekar Reddy    8 年前

    前言:

    重要提示:虽然它的投票率和接受率最高,但@staticsan接受的答案实际上是 不正确! -请参阅David Mulder的评论了解原因。

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

    因此,我将发布 详细介绍浏览器的功能和使用方法 setTimeout() 帮助 . 它看起来很长,但实际上非常简单和直截了当-我只是做了非常详细的说明。

    更新: 我做了一个jsiddle来演示下面的解释: http://jsfiddle.net/C2YBE/31/ . 许多 谢谢 感谢@thangchang帮助启动它。

    更新2: 为了防止JSfiddle网站死机或删除代码,我在最后将代码添加到这个答案中。


    细节 :

    想象一下一个带有“做点什么”按钮和“结果”部分的Web应用程序。

    这个 onClick “do something”按钮的处理程序调用函数“longcalc()”,该函数执行两项操作:

    1. 做一个很长的计算(假设需要3分钟)

    2. 将计算结果打印到结果部分。

    现在,你的用户开始测试这个,点击“做点什么”按钮,页面在那里停留了3分钟,他们变得焦躁不安,再次点击按钮,等待1分钟,什么都没有发生,再次点击按钮…

    问题很明显——你需要一个“状态”分区,它显示了正在发生的事情。让我们看看这是怎么回事。


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

    1. 填充状态“正在计算…可能需要3分钟进入状态分区

    2. 做一个很长的计算(假设需要3分钟)

    3. 将计算结果打印到结果部分。

    4. 将状态“Calculation Done”填充到状态DIV中

    而且,你很高兴地将这个应用程序交给用户重新测试。

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


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

    浏览器将事件产生的所有“todo”任务(包括ui任务和javascript命令)放入 单队列 . 不幸的是,用新的“calculating…”值重新绘制“status”DIV是一个单独的TODO,它将转到队列的末尾!

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

    • 队列: [Empty]
    • 事件:点击按钮。事件后队列: [Execute OnClick handler(lines 1-4)]
    • 事件:执行onclick处理程序中的第一行(例如change status div value)。事件后队列: [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”填充status div)。队列: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value] .
    • 事件:执行隐含 return 点击 handler sub.我们将“execute onclick handler”从队列中去掉,并开始执行队列中的下一项。
    • 注意:由于我们已经完成了计算,用户已经过了3分钟。 重新绘制事件还没有发生!!!!
    • 事件:用“计算”值重新绘制状态DIV。我们重新抽签,然后把它从队列中去掉。
    • 事件:用结果值重新绘制结果DIV。我们重新抽签,然后把它从队列中去掉。
    • 事件:用“完成”值重新绘制状态DIV。我们重新抽签,然后把它从队列中去掉。 敏锐的观察者甚至可能会注意到“status div”的“calculating”值会闪烁几微秒。- 计算完成后

    因此,根本的问题是“status”DIV的重新绘制事件被放置在队列末尾的“execute line 2”事件之后,这需要3分钟,所以实际的重新绘制直到计算完成之后才发生。


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

    因此,为了解决您的问题,您可以修改 点击 处理程序为两个语句(在新函数中或仅在 点击 ):

    1. 填充状态“正在计算…可能需要3分钟进入状态分区

    2. 执行 设置TimeTo() 超时为0,调用 朗卡尔() 功能 .

      朗卡尔() 函数与上一次几乎相同,但显然没有第一步的“计算…”状态DIV UPDATE,而是立即开始计算。

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

    • 队列: [空]
    • 事件:点击按钮。事件后队列: [Execute OnClick handler(status update, setTimeout() call)]
    • 事件:执行onclick处理程序中的第一行(例如change status div value)。事件后队列: [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)] .
    • 事件: 用“计算”值重新绘制状态分区 . 排队后: [execute LongCalc (lines 1-3)] . 请注意,这个重绘事件可能会在警报响起之前发生,这也同样有效。

    万岁!在开始计算之前,status div刚刚更新为“calculating…”!!!!



    下面是jfiddle中的示例代码,说明了这些示例: 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);
    });
    
        3
  •  85
  •   BalusC    14 年前

    看看约翰·雷西格的文章 How JavaScript Timers Work . 设置超时时,它实际上会将异步代码排队,直到引擎执行当前的调用堆栈。

        4
  •  21
  •   Zander    7 年前

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

    一些JavaScript执行和UI更新任务排队到浏览器消息队列,然后被发送到要执行的浏览器主线程。

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

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

        5
  •  20
  •   Jose Basilio    16 年前

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

    看看这个: setTimeout

        6
  •  18
  •   Val Kornea    10 年前

    这里有相互冲突的赞成的答案,没有证据就没有办法知道该相信谁。这里有证据表明@dvk是正确的,@salvadordali是错误的。后者声称:

    “这就是为什么:不可能有一个时间来安排 延迟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
  •  13
  •   Pointy    9 年前

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

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

        8
  •  9
  •   Salvador Dali    11 年前

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

    所以你有两个功能:

    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,超时小于4,则 将超时时间增加到4。

    同样来自Mozilla:

    要在现代浏览器中实现0 ms超时,可以使用 window.postmessage(),如前所述 here .

    阅读以下内容后获取P.S.信息 article .

        9
  •  8
  •   user113716    14 年前

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

        10
  •  3
  •   Jason Suárez    13 年前

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

        11
  •  3
  •   DanielSmedegaardBuus    7 年前

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

    什么 事实上 这里所说的并不是“浏览器可能还没有完全准备好,因为并发性”,或者基于“每行都是一个添加到队列后面的事件”的内容。

    这个 jsfiddle 由dvk提供确实说明了一个问题,但他的解释是不正确的。

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

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

    这就是它变得有趣的地方。我们已经习惯了把javascript看作是异步的,以至于我们很容易忽略这个小事实: 在执行下一帧之前,必须完全执行任何帧。 . 没有并发性,伙计们。

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

    那么,为什么这篇文章的副作用没有更新呢?因为当你 更改了dom中元素的值,您可以 console.log() 它的值在更改后立即显示 已更改(显示为什么dvk的解释不正确)浏览器正在等待堆栈耗尽 on handler函数返回),从而完成消息,以便它最终能够执行运行时添加的消息,作为对突变操作的反应,并在UI中反映该突变。

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

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

    为什么 设置超时 那事情就行了?它这样做,因为它有效地将对长时间运行函数的调用从其自身的框架中移除,并将其调度为稍后在 window 上下文,以便它本身可以 立即返回 并允许消息队列处理其他消息。其思想是,当我们在javascript中更改dom中的文本时,我们在javascript中触发的ui“on update”消息现在位于排队等待长时间运行函数的消息之前,以便在我们长时间阻塞之前进行ui更新。

    注意a)长期运行功能 静止块 当它运行时,一切都会发生;b)您不能保证UI更新实际上在消息队列中处于领先地位。在2018年6月的Chrome浏览器上, 0 不能“解决”小提琴所表现出的问题。实际上,我有点被这种情况所窒息,因为在我看来,UI更新消息应该在它之前排队,因为它的触发器是在调度长时间运行的函数“稍后”运行之前执行的。但也许V8引擎有些优化可能会干扰,或者我只是缺乏理解。

    好吧,那么使用的问题是什么? 设置超时 对于这个特定的案例,什么是更好的解决方案?

    首先,使用问题 设置超时 在任何类似这样的事件处理程序上,为了缓解另一个问题,很容易与其他代码混淆。以下是我工作中的一个现实例子:

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

    第一个问题是显而易见的;您不能线程化JavaScript,所以在添加模糊时,您在这里什么也得不到。其次,您现在已经有效地将模板的呈现从可能的事件侦听器堆栈中分离出来,这些侦听器可能期望已经呈现了某个模板,但很可能还没有呈现。该函数的实际行为现在是非确定性的,正如任何运行它或依赖它的函数在不知情的情况下都是非确定性的。你可以做出有根据的猜测,但是你不能正确地为它的行为编码。

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

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

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

        12
  •  2
  •   ChrisN    12 年前

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

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

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

        13
  •  1
  •   Jeremy    16 年前

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

        14
  •  1
  •   fabspro    12 年前

    其他一些设置超时很有用的情况:

    您希望将长时间运行的循环或计算拆分为较小的组件,这样浏览器就不会显示为“冻结”或说“页面上的脚本正忙”。

    您希望在单击时禁用表单提交按钮,但如果禁用onclick处理程序中的按钮,则不会提交表单。设置时间为零的技巧是,允许事件结束,表单开始提交,然后您的按钮可以被禁用。

        15
  •  1
  •   Stephan G    9 年前

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

    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();
    }
    
        16
  •  1
  •   Willem van der Veen    6 年前

    问题是您试图对一个不存在的元素执行一个javascript操作。元素尚未加载,并且 setTimeout() 为元素提供更多的加载时间,方法如下:

    1. 设置TimeTo() 使事件 同步的 因此,在所有同步代码之后执行,可以给元素更多的加载时间。异步回调,如中的回调 设置TimeTo() 放置在 事件队列 把它放在 事件循环 在同步代码堆栈为空之后。
    2. 函数中作为第二个参数的ms值0 设置TimeTo() 通常略高(4-10毫秒,取决于浏览器)。执行 设置TimeTo() 回调是由事件循环的“ticks”(如果堆栈为空,则tick正在堆栈上推送回调)量引起的。由于性能和电池寿命的原因,事件循环中的节拍数量被限制在一定数量。 较少的 每秒1000次以上。
        17
  •  0
  •   Sohail Yasmin    7 年前

    Javascript是单线程应用程序,因此不允许同时运行函数,因此可以使用此事件循环。所以,setTimeout(fn,0)所做的就是将其导入到任务请求中,当调用堆栈为空时执行任务请求。我知道这个解释很无聊,所以我建议你看这段视频,这将帮助你如何在浏览器的引擎盖下工作。 查看此视频: https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ