setInterval 计时慢慢偏离保持准确

IT技术 javascript setinterval
2021-01-23 09:51:39

似乎当我setInterval持续 1000 毫秒时,它实际上每 1001 毫秒左右触发一次该功能。这导致运行时间越长,时间漂移越慢。

var start;
var f = function() {
    if (!start) start = new Date().getTime();
    var diff = new Date().getTime() - start;
    var drift = diff % 1000;
    $('<li>').text(drift + "ms").appendTo('#results');
};

setInterval(f, 1000);

运行时,这会立即显示不准确。

  • 0ms
  • 1ms
  • 2ms
  • 3ms
  • 4ms
  • 5ms
  • 5ms
  • 7ms
  • 8ms
  • 9ms
  • 9ms
  • 10ms

自己看看:http : //jsfiddle.net/zryNf/

那么有没有更准确的计时方法呢?或者一种使setInterval行为更准确的方法?

6个回答

我想我可能已经找到了解决方案。我想,如果你能测量它,你就可以补偿它,对吗?

http://jsfiddle.net/zryNf/9/

var start;
var nextAt;

var f = function() {
    if (!start) {
        start = new Date().getTime();
        nextAt = start;
    }
    nextAt += 1000;

    var drift = (new Date().getTime() - start) % 1000;    
    $('<li>').text(drift + "ms").appendTo('#results');

    setTimeout(f, nextAt - new Date().getTime());
};

f();

结果略有不同,但这是最近的一次运行:

0ms
7ms
2ms
1ms
1ms
1ms
2ms
1ms
1ms
1ms

因此,如果它在 1 毫秒、2 毫秒甚至 10 毫秒后被调用,则应该安排下一次调用来补偿它。只要不准确只是每次调用,但时钟不应该浪费时间,那么这应该可以正常工作。


现在我把它包装成一个全局accurateInterval函数,它几乎替代了setInterval. https://gist.github.com/1d99b3cd81d610ac7351

我不认为夏令时在这里很重要。new Date().getTime() 返回自 UNIX 时代以来的 ms,它总是一致地向前推进(当然闰秒除外......)。
2021-03-17 09:51:39
@Phrogz 注意:当它向前冲刺时,这是整整一个小时的帧时间,而不是实时时间。
2021-03-21 09:51:39
另请注意,这new Date().getTime() - start等效于new Date - start.
2021-03-25 09:51:39
一个警告:夏令时。你要么让一帧延迟一小时(“向前”),要么让帧尽可能快地触发(负超时间隔)整整一个小时(“回退”)。
2021-03-28 09:51:39
夏令时肯定是一个有趣的边缘情况。但至少对于我的需要来说,这样的情况已经足够罕见了,而且这种结果的后果还不够严重,所以这并不是一个真正的问题。
2021-03-28 09:51:39

通过一些谷歌搜索,您会看到,setInterval并且settimeout两者都不会在您告诉它的确切指定时间执行代码。setInterval(f,1000);它会等待至少1000MS它执行前,也不会等待确切1000MS。其他进程也在等待轮到它们使用 CPU,这会导致延迟。如果您需要一个精确的计时器,时间为 1 秒。我会使用更短的时间间隔,比如 50MS,并将其与开始时间进行比较。我不会低于 50 毫秒,因为浏览器有最小间隔

这里有一些参考:

“为了理解定时器在内部是如何工作的,需要探索一个重要的概念:定时器延迟是不保证的。因为浏览器中的所有 JavaScript 都是在单线程异步事件(例如鼠标点击和定时器)上执行的当执行中有空缺时。最好用图表来证明,如下所示:”摘自:http : //css.dzone.com/news/how-javascript-timers-work

“Chrome 和 Chromium 提供的时间间隔平均略高于 41 毫秒,足以让第二个时钟在不到一分钟的时间内明显变慢。Safari 的时间略低于 41 毫秒,性能比 Chrome 好,但仍然不是很好。我在 Windows XP 下读取了这些读数,但 Chrome 在 Windows 7 下实际上表现更差,平均间隔约为 46 毫秒。” 取自:http : //www.goat1000.com/2011/03/23/how-accurate-is-window.setinterval.html

50ms 是 20 fps;Chrome 以 200fps 或 5ms 的速度上限回调;Firefox 和 IE 的运行速度至少会达到 250fps(我不确定它们是否有限制)。
2021-03-22 09:51:39
在我对这个主题的研究中,setInterval 确实试图通过提前触发来弥补失去的时间(无论如何在 firefox22 中)但它最终仍然漂移
2021-03-28 09:51:39
而不是使用 setInterval,尝试使用 setTimeout 但下一个超时时间将是 1000 + drift
2021-04-04 09:51:39
有趣,但超高的滴答率似乎是一种蛮力解决方案。
2021-04-10 09:51:39

这是另一个自动更正间隔。间隔设置为较短的时间段,然后等待至少一秒钟后触发。它不会总是在 1000 毫秒后触发(似乎在 0-6 毫秒的延迟范围内),但它会自动更正并且不会漂移。

编辑: 更新为使用召回setTimeout而不是setInterval在 1000 次左右的迭代后它可能会做一些奇怪的事情。

var start, tick = 0;
var f = function() {
    if (!start) start = new Date().getTime();
    var now = new Date().getTime();
    if (now < start + tick*1000) {
        setTimeout(f, 0);
    } else {
        tick++;
        var diff = now - start;
        var drift = diff % 1000;
        $('<li>').text(drift + "ms").appendTo('#results');
        setTimeout(f, 990);
    }
};

setTimeout(f, 990);

运行演示

我没有看到几乎和你的脚本报告一样大的漂移:http :
//jsfiddle.net/hqmLg/1/

我要让那个脚本继续运行。现在(Chrome,Win 7)我看到:

240.005s 中的 240 个调用是 0.99979 个调用/秒

事实上,我已经看到漂移上升到 0.007 秒,然后下降到 0.003 秒。我认为你的测量技术有缺陷。

在 Firefox 中,我看到它的漂移更加强烈(任一方向 +/- 8 毫秒),然后在下一次运行中进行补偿。大多数情况下,我在 Firefox 中看到“1.000000 个调用/秒”。

此脚本不计算“总”漂移。它,像所有其他人一样,不将负漂移视为漂移,相反,它使正漂移看起来更少。事实上,在运行 win7、ff22、一堆标签和潘多拉收音机时,无论哪种方式,我都看到了高达 150 毫秒的漂移。
2021-03-20 09:51:39
好吧,至少这证明这是 JS 引擎实现的高度可变的。
2021-03-29 09:51:39
@Squeegy 好数据点;我已经编辑以澄清我使用的是 Windows 7。在我看来,Chrome 漂移(有时向后,大部分是向前)并且不会尝试补偿它,但 Firefox 会。
2021-03-31 09:51:39
澄清一下,我的时间间隔从 900 毫秒到 1150 毫秒不等。firefox 似乎试图通过在下一个滴答声早期触发来弥补正漂移,但它仍然滞后并缓慢向前漂移(浪费时间)
2021-04-10 09:51:39
我在 Lion 上运行 Chrome beta 16.0.912.41,我看到更高的漂移:110 calls in 110.092s is 0.999164 calls/second. 每次调用几乎一毫秒。
2021-04-13 09:51:39

您可以使用此功能使呼叫接近预期的时间表。它使用setTimeout并根据经过的时间计算下一次调用时间。

function setApproxInterval(callback, interval) {
  let running = true
  const startTime = Date.now()

  const loop = (nthRun) => {
    const targetTime = nthRun * interval + startTime
    const timeout = targetTime - Date.now()
    setTimeout(() => {
      if (running) {
        callback()
        loop(nthRun + 1)
      }
    }, timeout)
  }

  loop(1)
  return () => running = false
}

function clearApproxInterval(stopInterval) {
  stopInterval()
}

// Example usage
const testStart = Date.now()
const interval = setApproxInterval(() => console.log(`${Date.now() - testStart}ms`), 1000)
setTimeout(() => clearApproxInterval(interval), 10000)