多个具有相同间隔的setTimeout()函数的执行顺序

37

考虑以下JavaScript代码:

function(){
    setTimeout(function() {
        $("#output").append(" one ");
    }, 1000);
    setTimeout(function() {
        $("#output").append(" two ");
    }, 1000);
}
你也可以在 jsfiddle 上查看此示例。
我能确定 #output 的值总是按这个顺序为 "one two" 吗?通常,我会像这样处理这个问题:
function(){
    setTimeout(function() {
        $("#output").append(" one ");
        $("#output").append(" two ");
    }, 1000));
}

但我无法这样做,因为我从服务器接收到消息,告诉我要执行哪个函数(在此示例中是追加"one"或追加"two"),我必须稍微延迟后执行。

我已经在Internet Explorer 9、Firefox 14、Chrome 20和Opera 12中测试了这段代码,输出始终是"one two",但我能确定这总是这样吗?


1
你为什么需要延迟执行? - bart s
在这里,您可以找到HTML 5中计时器的规范:http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#timers - some
一个解决方案是延迟计时器本身。我提出了一个可能的解决方案。 - Jibi Abraham
如果您关心执行顺序,您可能需要查看此页面http://www.onsip.com/blog/2012/06/29/avoiding-javascript-settimeout-and-setinterval-problems - Roland Pihlakas
可能是Javascript中相同的超时按顺序执行吗?的重复问题。 - Roland Pihlakas
9个回答

17

这里是规范文档

我理解在第7.3节中的setTimeout步骤8中,执行顺序被保证。

然而,在Chrome浏览器中,当窗口最小化再最大化时,来自外部源(如websockets或webworkers)的事件设置的超时时间会按错误的顺序执行。我认为这是浏览器的错误,并且希望很快会得到修复。


1
谢谢您指出规范!您的经验告诉我不要依赖它,这对我很有帮助。 - Uooo
我无法在提供的链接中找到7.3节。内容是否已更改?此外,我找到了以下文本:“此API不能保证计时器按计划准确运行。由于CPU负载、其他任务等原因可能会出现延迟。” - Roland Pihlakas
2
是的,现在已经过了相当长的时间,规范文档也发生了变化。问题不在于计时器是否按照预定时间精确触发,而更多地在于它们是否按照预期顺序触发,即使有延迟也是如此。 - kybernetikos
我相信由于Javascript的“轮换”特性,Websockets或Webworkers将在不同的轮次中执行,如果触发#two加法的websocket/worker返回得太早,它的轮次可能会比原本预期的更快。 - Keith Mattix
当时,在Chrome中,来自同一源(websocket)的多个事件在窗口最小化/最大化期间以错误的顺序执行。 - kybernetikos

4

在你的代码测试平台中尝试这个功能

$(document).ready(function() {
    setTimeout(function() {
        $("#output").append(" one ");
    }, 1000);
});
$(document).ready(function() {
    setTimeout(function() {
        $("#output").append(" two ");
    }, 999);
});​

您会发现两者都

output: one two
output: two one

可能发生的情况。因此,Speransky是正确的,您不能依赖于您的超时始终按相同顺序执行。

请注意,我已经将一个时间更改为1ms,以显示1000ms超时可以在999ms超时之前执行。

编辑:下面的代码可以延迟执行,而没有任何机会在one之前打印two

function(){
    setTimeout(function() {
        $("#output").append(" one ");
       setTimeout(function() {
           $("#output").append(" two ");
       }, 100);
    }, 1000);
}

6
这不是OP所问的,他问题中的setTimeout执行顺序已经确定,而你提供的例子中并没有固定的执行顺序。 - xdazz
1
如果插入第一个计时器的时间超过1毫秒,则结果为“one tow”。但如果时间少于1毫秒,则结果为“two one”。 - xiaowl
请查看上面some评论中的链接,了解关于“此API不能保证计时器按照预定时间运行。由于CPU负载、其他任务等原因,可能会出现延迟。”以及有关用户代理时间的第9点。尽管JavaScript在单个线程上执行,但行为可能与您的期望不同。我正在使用Chrome 20.0.1132.57 m进行测试。您可能正在使用不同的浏览器。当我们玩弄这段代码时,它会执行不同的操作。如果您依赖执行顺序,则非常危险! - bart s
我认为“不保证准确按计划执行”意味着setTimeout(func, 1000)定时器不能保证在1000毫秒后准确执行。 - xiaowl
来自 some 链接的文档说明无法保证计时器会准确按照时间表运行,用户代理时间也有类似的说法。这意味着如果我想要一个1000毫秒的超时,有时可能是900毫秒,有时可能是1100毫秒,但如果我仍然可以确保执行顺序(“one two”),那么这对我来说并不重要。xiaowl 发布的链接表示 JavaScript 使用的计时器已经保证了这一点。我的理解正确吗?我只需要依赖于“one two”的顺序,无论“two”是否在“one”之后400毫秒打印出来。 - Uooo
显示剩余3条评论

3
因为 JavaScript 代码在单线程中执行,所以所有异步事件(例如 click、mousemove)都排队等待执行。当您调用 setTimeout 时,引擎会将定时器插入其队列以在未来执行,至少要延迟一定的时间。因此,两个 setTimeout 会生成两个定时器,一个接一个地运行。
您可以查看 John Resig 的 How Javascript Timers Work

2

不,你不能确定。这是异步的。

但实际上,由于浏览器中机制的实现,它很可能是正确的。


1

是的,输出将始终为"one two"

这是因为第一个 setTimeout 总是在第二个 setTimeout 之前运行,如果它们具有相同的延迟时间,则第一个的回调函数始终会在第二个的回调函数之前执行。


@SperanskyDanil 即使差异小于1毫秒,但顺序是固定的。 - xdazz

0

更新
我用这种方法更新了您的小样。点击这里查看

还有一个方法,从服务器上获取待办事项列表 --

//Example - append one, append two
var appendList = ['one', 'two']

//then do this
appendList.forEach(function(item, index){
    setTimeout(function(){ 
        $('#output').append(item)
    }, index * 50 + 1000);
});

这样,您可以决定顺序。


那并没有回答我的原始问题。我只是想解释一下为什么我不能使用我在问题中展示的第二种方法。如果我无法使用相同间隔的setTimeout()确保执行顺序,那么我必须寻找另一种适合我的应用程序的解决方案。 - Uooo
但我不能使用第二种解决方案。 - Uooo

0
如果事件是同步的,那么可以使用Continuum函数按顺序运行函数:
function keyedSequence(key, fn) {
  fn = fn || this;
  key.push({fn:fn});

  return function() {
    for(var i=0, n, full=1; i<key.length; i++) {
      n = key[i];
      if(n.fn == fn) {
        if(!n.called) n.called = 1, n.args = key.slice.call(arguments);
        if(!full) break
      }
      if(!n.called) full = 0
    }

    if(full) for(i=0; i<key.length; i++)
      n = key[i], key[i] = {fn:n.fn}, n.fn.apply(n, n.args);
  }
}
Function.prototype.seq = keyedSequence;

你可以提供一个空数组作为键。拥有相同键的函数将会被分组在一起。

window.onload = function() {
  var key = [];
  document.getElementById("test1").addEventListener('click', function1.seq(key), false);
  document.getElementById("test2").addEventListener('click', function2.seq(key), false);
}

点击 test2,然后点击 test1,执行顺序仍然是 function1 然后是 function2

另一种调用方式是:

window.onload = function() {
  var key = [];
  document.getElementById("test1").addEventListener('click', keyedSequence(key, function1), false);
  document.getElementById("test2").addEventListener('click', keyedSequence(key, function2), false);
}

0

这取决于浏览器,因为setTimout是浏览器中window对象的一部分,而不是在ECMAScript中定义的。


0
根据规范,它们具有相同的任务来源same task source,具有相同任务来源的事物应该被排序。所以是的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接