我最近遇到了一个相当严重的错误,其中代码通过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()。它看起来很长,但实际上非常简单和直接——我只是把它做得非常详细。

更新:我已经做了一个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);
});

其他回答

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

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

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

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

看看这个:setTimeout

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

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毫秒后。

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

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

所以你有两个功能:

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()如下所述。

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

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