我最近遇到了一个相当严重的错误,其中代码通过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()?


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

看看这个:setTimeout


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


在问题中,存在以下之间的竞争条件:

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

您的代码总是在这场竞赛中胜出,并试图在浏览器准备好之前设置下拉选择,这意味着会出现错误。

这种竞争的存在是因为JavaScript有一个与页面呈现共享的执行线程。实际上,运行JavaScript会阻塞DOM的更新。

你的解决方案是:

setTimeout(callback, 0)

使用回调调用setTimeout,并将0作为第二个参数,将在尽可能短的延迟之后调度回调异步运行——当选项卡有焦点且JavaScript执行线程不忙时,大约为10ms。

因此,OP的解决方案是将所选索引的设置延迟约10ms。这让浏览器有机会初始化DOM,修复bug。

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


请参阅Philip Roberts的“到底什么是事件循环?”以获得更详细的解释。


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


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

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


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


前言:

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

因此,我将详细介绍浏览器的功能以及如何使用setTimeout()。它看起来很长,但实际上非常简单和直接——我只是把它做得非常详细。

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

UPDATE2:为了防止JSFiddle网站崩溃或删除代码,我在最后将代码添加到这个答案中。


细节:

想象一个带有“do something”按钮和结果div的web应用程序。

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

计算时间很长(比如3分钟) 将计算结果打印到结果div中。

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

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


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

填充状态“正在计算…”可能需要大约3分钟”进入状态DIV 计算时间很长(比如3分钟) 将计算结果打印到结果div中。 将状态“计算完成”填充到状态DIV中

而且,你很乐意让用户重新测试应用程序。

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


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

浏览器将事件产生的所有“TODO”任务(包括UI任务和JavaScript命令)放入单个队列。不幸的是,用新的“calculation…”值重新绘制“Status”DIV是一个单独的TODO,它会走到队列的末尾!

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

Queue: [Empty] Event: Click the button. Queue after event: [Execute OnClick handler(lines 1-4)] Event: Execute first line in OnClick handler (e.g. change Status DIV value). Queue after event: [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!!! Details explained below. Event: Execute second line in handler (calculation). Queue after: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]. Event: Execute 3rd line in handler (populate result DIV). Queue after: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]. Event: Execute 4th line in handler (populate status DIV with "DONE"). Queue: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]. Event: execute implied return from onclick handler sub. We take the "Execute OnClick handler" off the queue and start executing next item on the queue. NOTE: Since we already finished the calculation, 3 minutes already passed for the user. The re-draw event didn't happen yet!!! Event: re-draw Status DIV with "Calculating" value. We do the re-draw and take that off the queue. Event: re-draw Result DIV with result value. We do the re-draw and take that off the queue. Event: re-draw Status DIV with "Done" value. We do the re-draw and take that off the queue. Sharp-eyed viewers might even notice "Status DIV with "Calculating" value flashing for fraction of a microsecond - AFTER THE CALCULATION FINISHED

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


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

所以,为了解决你的问题,你修改你的onClick处理程序为两个语句(在一个新函数或只是onClick内的一个块):

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

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

Queue: [Empty] Event: Click the button. Queue after event: [Execute OnClick handler(status update, setTimeout() call)] Event: Execute first line in OnClick handler (e.g. change Status DIV value). Queue after event: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]. Event: Execute second line in handler (setTimeout call). Queue after: [re-draw Status DIV with "Calculating" value]. The queue has nothing new in it for 0 more seconds. Event: Alarm from the timeout goes off, 0 seconds later. Queue after: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]. Event: re-draw Status DIV with "Calculating" value. Queue after: [execute LongCalc (lines 1-3)]. Please note that this re-draw event might actually happen BEFORE the alarm goes off, which works just as well. ...

万岁!在计算开始之前,状态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);
});

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


setTimeout有用的其他一些情况:

你想要把一个长时间运行的循环或计算分解成更小的组件,这样浏览器就不会出现“冻结”或说“页面上的脚本正在忙”。

您希望在单击时禁用表单提交按钮,但如果禁用onClick处理程序中的按钮,则表单将无法提交。setTimeout的时间为0,允许事件结束,表单开始提交,然后可以禁用按钮。


浏览器有一个叫做“主线程”的进程,它负责执行一些JavaScript任务,UI更新,例如:绘制,重绘,回流等等。 JavaScript任务被排队到消息队列中,然后分派到浏览器的主线程中执行。 当在主线程繁忙时生成UI更新时,任务被添加到消息队列中。


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

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

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


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

所以你有两个功能:

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毫秒。历史上浏览器将这个最小值设置为10毫秒,但HTML5规范和现代浏览器将其设置为4毫秒。

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

同样来自mozilla:

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

附注:信息是在阅读以下文章后获取的。


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

这就是为什么:它不可能有一个时间setTimeout 延迟0毫秒。最小值由 而不是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

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

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


这两个排名靠前的答案都是错的。查看关于并发模型和事件循环的MDN描述,应该会清楚发生了什么(MDN资源是一个真正的珍宝)。除了“解决”这个小问题之外,简单地使用setTimeout可能会在代码中添加意想不到的问题。

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

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

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

然后,当您实际单击按钮时,将创建引用事件处理程序函数的消息,该消息被添加到消息队列中。当事件循环到达这条消息时,它在堆栈上创建一个框架,并调用jsfiddle中的click事件处理程序。

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

What does this mean? It means that whenever a function is invoked from the message queue, it blocks the queue until the stack it generates has been emptied. Or, in more general terms, it blocks until the function has returned. And it blocks everything, including DOM rendering operations, scrolling, and whatnot. If you want confirmation, just try to increase the duration of the long running operation in the fiddle (e.g. run the outer loop 10 more times), and you'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 making the page unresponsive. The frame is being executed, and the event loop and message queue are stuck until it finishes.

So why this side-effect of the text not updating? Because while you have changed the value of the element in the DOM — you can console.log() its value immediately after changing it and see that it has been changed (which shows why DVK's explanation isn't correct) — the browser is waiting for the stack to deplete (the on handler function to return) and thus the message to finish, so that it can eventually get around to executing the message that has been added by the runtime as a reaction to our mutation operation, and in order to reflect that mutation in the UI.

This is because we are actually waiting for code to finish running. We haven't said "someone fetch this and then call this function with the results, thanks, and now I'm done so imma return, do whatever now," like we usually do with our event-based asynchronous Javascript. We enter a click event handler function, we update a DOM element, we call another function, the other function works for a long time and then returns, we then update the same DOM element, and then we return from the initial function, effectively emptying the stack. And then the browser can get to the next message in the queue, which might very well be a message generated by us by triggering some internal "on-DOM-mutation" type event.

浏览器UI不能(或选择不)更新UI,直到当前执行的帧已经完成(函数已经返回)。我个人认为,这与其说是限制,不如说是有意为之。

Why does the setTimeout thing work then? It does so, because it effectively removes the call to the long-running function from its own frame, scheduling it to be executed later in the window context, so that it itself can return immediately and allow the message queue to process other messages. And the idea is that the UI "on update" message that has been triggered by us in Javascript when changing the text in the DOM is now ahead of the message queued for the long-running function, so that the UI update happens before we block for a long time.

Note that a) The long-running function still blocks everything when it runs, and b) you're not guaranteed that the UI update is actually ahead of it in the message queue. On my June 2018 Chrome browser, a value of 0 does not "fix" the problem the fiddle demonstrates — 10 does. I'm actually a bit stifled by this, because it seems logical to me that the UI update message should be queued up before it, since its trigger is executed before scheduling the long-running function to be run "later". But perhaps there're some optimisations in the V8 engine that may interfere, or maybe my understanding is just lacking.

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

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

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

First problem is obvious; you cannot thread javascript, so you win nothing here while you add obfuscation. Secondly, you have now effectively detached the rendering of a template from the stack of possible event listeners that might expect that very template to have been rendered, while it may very well not have been. The actual behaviour of that function was now non-deterministic, as was — unknowingly so — any function that would run it, or depend on it. You can make educated guesses, but you cannot properly code for its behaviour.

The "fix" when writing a new event handler that depended on its logic was to also use setTimeout 0. But, that's not a fix, it is hard to understand, and it is no fun to debug errors that are caused by code like this. Sometimes there's no problem ever, other times it concistently fails, and then again, sometimes it works and breaks sporadically, depending on the current performance of the platform and whatever else happens to going on at the time. This is why I personally would advise against using this hack (it is a hack, and we should all know that it is), unless you really know what you're doing and what the consequences are.

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

哦,如果你在想,“我能不能在长时间运行的函数中加入一个回调,使它异步?,然后没有。回调并没有使它异步化,在显式调用回调之前,它仍然必须运行长时间运行的代码。


问题是您试图在一个不存在的元素上执行Javascript操作。元素还没有被加载,setTimeout()给了一个元素更多的时间,以以下方式加载:

setTimeout() causes the event to be ansynchronous therefore being executed after all the synchronous code, giving your element more time to load. Asynchronous callbacks like the callback in setTimeout() are placed in the event queue and put on the stack by the event loop after the stack of synchronous code is empty. The value 0 for ms as a second argument in function setTimeout() is often slightly higher (4-10ms depending on browser). This slightly higher time needed for executing the setTimeout() callbacks is caused by the amount of 'ticks' (where a tick is pushing a callback on the stack if stack is empty) of the event loop. Because of performance and battery life reasons the amount of ticks in the event loop are restricted to a certain amount less than 1000 times per second.


如果你不想看完整个视频,这里有一个简单的解释,你需要理解的东西,为了能够理解这个问题的答案:

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

从这一点开始,我们讨论的是“浏览器中的”JavaScript。像setTimeout这样的东西确实是浏览器的东西,而不是JavaScript本身的一部分。

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

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

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

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

值得庆幸的是,这并不是浏览器设计者设计它们工作的方式。相反,这个调用(setTimeout(fn, 5000);)是立即完成的。这一点非常重要:即使有5000毫秒的延迟,这个函数调用也会在瞬间完成!接下来会发生什么?它被从主空间中移除。演出地点在哪里?(因为我们不想失去它)。您可能猜对了:浏览器听到这个调用并把它放在第二个空格上。

浏览器会跟踪5秒的延迟,一旦它过去了,它就会查看主空间,“当它是空的”,把fn();回调它。这就是setTimeout的工作方式。

回到setTimeout(fn, 0),即使延迟为0,这仍然是对浏览器的调用,浏览器会立即听到它并接收它,把它放在第二个空间上,只有当主空间再次为空时才把它放回主空间,而不是真正的0毫秒后。

我真的建议大家也去看看那个视频,因为他解释得非常好,而且更多地讲解了技术方面的东西。


//When need "new a", setTimeout(fn, 0) is useful, when need to wait some action. Example: var a = function (){console.log('a');}; var b = function(){setTimeout(b, 100);}; //wait some action before override this function //without setTimeout: console.log('no setTimeout: b.toString():', b.toString()); b(); //"b" is an old function console.log('no setTieout: a.toString(): ', a.toString()); a(); //and "a" is not overrided setTimeout(//but with setTimeout(fn, 0): function(){ console.log('After timeout 0, b.toString(): ', b.toString()); b(); //"b" is a new function console.log('After timeout 0, a.toString(): ', a.toString()); a(); //and "a" is overrided }, 0 ); //override var "b", which was been undefined b = function (){ a = function(){console.log('new a');}; }