JavaScript的setTimeout为什么会如此不准确?

46

我有这段代码:

var date = new Date();
setTimeout(function(e) {
    var currentDate = new Date();
    if(currentDate - date >= 1000) {
         console.log(currentDate, date);
         console.log(currentDate-date);
    }
    else {
       console.log("It was less than a second!");
       console.log(currentDate-date);
    }
}, 1000);

在我的电脑上,代码总是能正确执行,控制台输出1000。有趣的是,在其他电脑上,同样的代码会在不到一秒钟内启动超时回调,并且currentDate - date之间的差异在980和998之间。

我知道存在解决此精度问题的库(例如Tock)。

基本上,我的问题是:setTimeout为什么不能按照给定的延迟触发?这可能是因为电脑太慢,浏览器自动尝试适应缓慢并在事件之前触发吗?

PS:以下是在Chrome JavaScript控制台中执行的代码和结果的截图:

Enter image description here


1
1-2毫秒(超过1000毫秒)的差异可能归因于抖动。您为什么认为这是不准确的?这只占了1/5的百分比。 - Elliott Frisch
7
约翰·里西格(John Resig)写了一篇很不错的文章 :) http://ejohn.org/blog/accuracy-of-javascript-time/ - rorypicko
1
@Tomás 这取决于你想要多精确。说你想要它“完全”是1秒钟不现实的。你得到的999毫秒在百分位小数上是准确的,但可能不是千分之一。总会有一定程度的不准确性。 - Zhihao
1
因为浏览器不是实时系统。 - Dave Newton
2
有趣的一点是...我能够在Chrome 31.0.1650.63 m上重现_少于指定持续时间_的问题。然而,在34.0.1782.2 canary和我尝试过的其他所有浏览器中,我都收到了>= 1000(如预期)。 - canon
显示剩余2条评论
5个回答

30

它不应该特别准确。有许多因素限制了浏览器执行代码的时间; 引用自MDN

除了“夹紧”之外,当页面(或操作系统/浏览器本身)忙于其他任务时,延迟时间还可能更晚。

换句话说,通常实现setTimeout的方式只是为了在给定的延迟时间后执行,并且一旦浏览器的线程可以自由执行它。

然而,不同的浏览器可能会以不同的方式实现它。这是我做的一些测试:

var date = new Date();
setTimeout(function(e) {
    var currentDate = new Date();
    console.log(currentDate-date);
}, 1000);

// Browser Test1 Test2 Test3 Test4
// Chrome    998  1014   998   998
// Firefox  1000  1001  1047  1000
// IE 11    1006  1013  1007  1005

或许在 Chrome 中少于 1000 次的调用是由于 Date 类型的不准确,或者可能是 Chrome 使用了不同的策略来决定何时执行代码——也许它正在尝试将其放入最近的时间槽中,即使超时延迟尚未完成。

总之,如果您希望实现可靠、一致、毫秒级的定时,请不要使用 setTimeout


2
简而言之,如果您期望可靠、一致、毫秒级的任何东西,就不应该使用 JavaScript。在这种语言中,确实没有其他选择。 - Josh Hibschman

21

一般来说,当计算机程序试图执行高精度操作时,其可靠性很低,通常不超过50毫秒。原因是即使在八核心的超线程处理器上,操作系统通常也要处理数百个进程和线程,有时甚至是数千个以上。操作系统通过将它们安排在一个接一个地获取CPU时间片段的时间表中来使所有这些多任务工作,这意味着它们最多只有几毫秒的时间来做它们的事情。

隐含的意思是,如果您设置了1000毫秒的超时,那么当前浏览器进程可能甚至在那个时间点都没有运行,因此,浏览器直到1005、1010甚至1050毫秒才注意到它应该执行给定的回调函数是完全正常的。

通常这不是问题,发生这种情况很少是非常重要的。如果确实如此,所有操作系统都提供内核级计时器,比1毫秒要精确得多,并允许开发人员在精确恰当的时间点执行代码。然而,作为一个受到严格限制的环境,JavaScript没有访问内核对象的权限,而浏览器也不会使用它们,因为理论上它可以通过仔细构造代码,使用大量危险的计时器淹没其他线程,从而攻击操作系统的稳定性。

至于为什么测试结果是980,我不确定 - 这取决于您使用的浏览器和JavaScript引擎。然而,我完全理解如果浏览器手动向下修正了一点系统负载和/或速度,以确保“平均延迟仍然约为正确时间” - 从沙盒原则来看,这将在不损害系统的情况下近似估算所需的时间。


实际上,这个问题并不特定于JavaScript。对于一个执行解释语言的浏览器来说,50毫秒或更短的准确度已经相当不错了。 - Alexis Wilke
嗯...我可以在Golang和C中实现相当可靠的纳秒级时间精度。也许这更与编程语言相关,而不是一般的计算? - Josh Hibschman
@JoshHibschman,自原回答发布以来的8年多时间里,计算机和处理器有所改进。然而,纳秒级精度仍然是不可能的,Golang和C也无法免除上下文切换的影响。一旦内核暂停了您的进程,无论使用任何语言都会受到影响。 - Niels Keurentjes
仅仅是在这里戳一下单线程语言,这是上下文切换的主要限制,即你无法决定下一个时钟周期会发生什么。然而在C/Go等语言中,只需在两个线程之间共享一个互斥锁,一个线程负责计时,另一个线程负责工作。如果您的系统可以在小于1秒的时间内循环十亿次,则可以实现纳秒精度。 - Josh Hibschman

7

如果我错误解释了这些信息,请有人纠正我:

根据John Resig的一篇文章,关于不同平台上性能测试的不准确性(重点是我的)

由于系统时间不断地向下舍入到上次查询的时间(大约相差15ms),性能结果的质量严重受损。

因此,在与系统时间比较时,每端都可能有多达15ms的误差。


2
这是问题的一个方面。另一个问题是浏览器的异步性,因此根据队列中的其他任务,超时可能会被暂停以处理其他工作。 - EmptyArsenal

1

我有过类似的经历。
我曾经使用过类似这样的东西:

var iMillSecondsTillNextWholeSecond = (1000 - (new Date().getTime() % 1000));
setTimeout(function ()
{
    CountDownClock(ElementID, RelativeTime);
}, iMillSecondsTillNextWholeSecond);//Wait until the next whole second to start.

我注意到它会每隔几秒钟跳过一秒,有时它会持续更长时间。然而,即使过了10或20秒,我仍然能看到它跳过。我想:“也许超时太慢或在等待其他东西?”然后我意识到:“也许它太快了,浏览器管理的计时器差了几毫秒?”在将我的变量加上+1毫秒后,我只看到了它跳过一次。最终,我增加了+50ms,以保险起见。
var iMillSecondsTillNextWholeSecond = (1000 - (new Date().getTime() % 1000) + 50);

我知道这有点取巧,但是我的计时器现在运行得很顺畅。 :)


1
是的,最终我到这里来了,因为我的setTimeout在chrome中提前了20ms。 - Josh Hibschman

0
JavaScript有一种处理精确时间框架的方法。以下是一种方法:
你可以在开始等待时保存一个`Date.now`,然后创建一个低毫秒更新帧的间隔,并计算日期之间的差异。
示例:
const startDate = Date.now()

const interval = setInterval(() => {
  const currentDate = Date.now()

  if (currentDate - startDate === 1000 {
    // it was a second

    clearInterval(interval)
    return
  } 

  // it was not a second
}, 50)

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