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


当前回答

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

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

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

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

看看这个:setTimeout

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

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

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

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

你的解决方案是:

setTimeout(callback, 0)

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

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

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


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

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

setTimeout有用的其他一些情况:

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

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