如何在javascript中创建一个准确的计时器?

IT技术 javascript time setinterval
2021-01-15 09:05:29

我需要创建一个简单但准确的计时器。

这是我的代码:

var seconds = 0;
setInterval(function() {
timer.innerHTML = seconds++;
}, 1000);

正好 3600 秒后,它打印大约 3500 秒。

  • 为什么不准确?

  • 如何创建一个准确的计时器?

6个回答

为什么不准确?

因为您正在使用setTimeout()setInterval()他们不能被信任,他们没有准确性保证。它们可以 任意滞后,它们不会保持恒定的速度,而是倾向于漂移(如您所见)。

如何创建一个准确的计时器?

改用Date对象来获取(毫秒)准确的当前时间。然后将您的逻辑基于当前时间值,而不是计算您的回调执行的频率。

对于一个简单的定时器或时钟,跟踪时间的差异明确:

var start = Date.now();
setInterval(function() {
    var delta = Date.now() - start; // milliseconds elapsed since start
    …
    output(Math.floor(delta / 1000)); // in seconds
    // alternatively just show wall clock time:
    output(new Date().toUTCString());
}, 1000); // update about every second

现在,这有可能跳跃值的问题。当间隔稍微滞后并在990, 1993, 2996, 3999,5002毫秒后执行回调时,您将看到第二个计数0, 1, 2, 3, 5(!)。因此,建议更频繁地更新,例如大约每 100 毫秒更新一次,以避免此类跳转。

然而,有时你真的需要一个稳定的时间间隔来执行你的回调而不会漂移。这需要更高级的策略(和代码),尽管它的回报很好(并且注册的超时更少)。这些被称为自调整定时器。与预期间隔相比,这里每个重复超时的确切延迟适应实际经过的时间:

var interval = 1000; // ms
var expected = Date.now() + interval;
setTimeout(step, interval);
function step() {
    var dt = Date.now() - expected; // the drift (positive for overshooting)
    if (dt > interval) {
        // something really bad happened. Maybe the browser (tab) was inactive?
        // possibly special handling to avoid futile "catch up" run
    }
    … // do what is to be done

    expected += interval;
    setTimeout(step, Math.max(0, interval - dt)); // take into account drift
}
上面写的计时器几乎总是会比预期的时间晚一点。如果您只需要准确的间隔,这很好,但如果您是相对于其他事件的时间,那么您将始终有这么小的延迟。要纠正这一点,您还可以跟踪漂移历史记录并添加二次调整,使漂移以目标时间为中心。例如,如果您的漂移为 0 到 30 毫秒,这会将其调整为 -15 到 +15 毫秒。我用滚动中位数来解释随着时间的推移而发生的变化。仅取 10 个样本就可以产生合理的差异。
2021-04-04 09:05:29
这在很大程度上取决于您的计时器在做什么。大多数情况下,有比step一次执行多次更好的方法如果您使用差异策略(而不是计数器) - 无论计时器是否自动调整,您都应该这样做 - 您只需要一个步骤,它就会自动赶上。
2021-04-06 09:05:29
@nem035 哦,有足够多的问题我还没有回答:-)
2021-04-06 09:05:29
@JoshuaMichaelCalafell 取决于您的用例,当步骤严重延迟时该怎么办。如果您什么都不做,它会尝试通过尽可能快地触发尽可能多的步骤来追赶,这实际上可能对动画等不利。您可以这样做expected += dt(与 相同expected = Date.now())或者expected += interval * Math.floor(dt / interval)只是跳过错过的步骤 - 可能对与 delta 一起使用的所有内容。如果您以某种方式计算步数,那么您可能需要进行调整。
2021-04-07 09:05:29
对第二个计时器代码的一些解释真的很有帮助
2021-04-07 09:05:29

我只是建立在Bergi 的回答(特别是第二部分)的基础上,因为我真的很喜欢它的完成方式,但是我希望可以选择在计时器启动后停止计时器(clearInterval()几乎就像)。Sooo...我已经把它包装成一个构造函数,所以我们可以用它做“客观”的事情。

1. 构造函数

好的,所以你复制/粘贴...

/**
 * Self-adjusting interval to account for drifting
 * 
 * @param {function} workFunc  Callback containing the work to be done
 *                             for each interval
 * @param {int}      interval  Interval speed (in milliseconds)
 * @param {function} errorFunc (Optional) Callback to run if the drift
 *                             exceeds interval
 */
function AdjustingInterval(workFunc, interval, errorFunc) {
    var that = this;
    var expected, timeout;
    this.interval = interval;

    this.start = function() {
        expected = Date.now() + this.interval;
        timeout = setTimeout(step, this.interval);
    }

    this.stop = function() {
        clearTimeout(timeout);
    }

    function step() {
        var drift = Date.now() - expected;
        if (drift > that.interval) {
            // You could have some default stuff here too...
            if (errorFunc) errorFunc();
        }
        workFunc();
        expected += that.interval;
        timeout = setTimeout(step, Math.max(0, that.interval-drift));
    }
}

2. 实例化

告诉它该做什么等等...

// For testing purposes, we'll just increment
// this and send it out to the console.
var justSomeNumber = 0;

// Define the work to be done
var doWork = function() {
    console.log(++justSomeNumber);
};

// Define what to do if something goes wrong
var doError = function() {
    console.warn('The drift exceeded the interval.');
};

// (The third argument is optional)
var ticker = new AdjustingInterval(doWork, 1000, doError);

3.然后做...东西

// You can start or stop your timer at will
ticker.start();
ticker.stop();

// You can also change the interval while it's in progress
ticker.interval = 99;

我的意思是,无论如何它对我有用。如果有更好的方法,让我知道。

当用户长时间离开浏览器选项卡/窗口(例如 chrome)时,此方法是否也有效?(据我所知,超过 10-20 分钟足以“阻止”计时器)
2021-03-13 09:05:29
我知道这篇文章有点旧,但很好奇是否有人有关于如何调整它以便元素可以拥有自己的计时器的想法。因此,当您滚动页面时,您可以让各种元素计算它们的时间。这是我试图解决的问题,如下所示:stackoverflow.com/questions/70019023/...
2021-03-29 09:05:29
@AndreiF 不,您不能在选项卡处于非活动状态时依赖超时。您需要做的是在开始时创建一个时间戳,然后让每个间隔/超时创建一个新的时间戳并减去开始时间戳以查看自开始以来实际过去了多少时间。因此,例如,如果您正在制作秒表,则不会使用超时的延迟来测量时间的流逝。您将使用时间戳进行测量,间隔延迟就是您根据当前持续时间更新事物(例如手部运动)的频率。
2021-03-30 09:05:29
如果您愿意使用 RxJS,您可以使用 Observable.timer() 和各种运算符来实现类似的解决方案:stackblitz.com/edit/...
2021-04-04 09:05:29

此处答案中的大多数计时器都会滞后于预期时间,因为它们将“预期”值设置为理想值,并且仅考虑浏览器在此之前引入的延迟。如果您只需要准确的间隔,这很好,但是如果您是相对于其他事件的时间,那么您(几乎)总是会有这种延迟。

要纠正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用这种先发制人的校正添加二次调整,漂移的方差以目标时间为中心。例如,如果您总是有 20 到 40 毫秒的漂移,则此调整会将其移动到目标时间附近的 -10 到 +10 毫秒。

基于Bergi 的回答,我在预测算法中使用了滚动中位数。使用此方法仅采集 10 个样本即可产生合理的差异。

var interval = 200; // ms
var expected = Date.now() + interval;

var drift_history = [];
var drift_history_samples = 10;
var drift_correction = 0;

function calc_drift(arr){
  // Calculate drift correction.

  /*
  In this example I've used a simple median.
  You can use other methods, but it's important not to use an average. 
  If the user switches tabs and back, an average would put far too much
  weight on the outlier.
  */

  var values = arr.concat(); // copy array so it isn't mutated
  
  values.sort(function(a,b){
    return a-b;
  });
  if(values.length ===0) return 0;
  var half = Math.floor(values.length / 2);
  if (values.length % 2) return values[half];
  var median = (values[half - 1] + values[half]) / 2.0;
  
  return median;
}

setTimeout(step, interval);
function step() {
  var dt = Date.now() - expected; // the drift (positive for overshooting)
  if (dt > interval) {
    // something really bad happened. Maybe the browser (tab) was inactive?
    // possibly special handling to avoid futile "catch up" run
  }
  // do what is to be done
       
  // don't update the history for exceptionally large values
  if (dt <= interval) {
    // sample drift amount to history after removing current correction
    // (add to remove because the correction is applied by subtraction)
      drift_history.push(dt + drift_correction);

    // predict new drift correction
    drift_correction = calc_drift(drift_history);

    // cap and refresh samples
    if (drift_history.length >= drift_history_samples) {
      drift_history.shift();
    }    
  }
   
  expected += interval;
  // take into account drift with prediction
  setTimeout(step, Math.max(0, interval - dt - drift_correction));
}

Bergi的回答准确指出了问题中的计时器不准确的原因。以下是一张上用一个简单的JS计时器startstopresetgetTime方法:

class Timer {
  constructor () {
    this.isRunning = false;
    this.startTime = 0;
    this.overallTime = 0;
  }

  _getTimeElapsedSinceLastStart () {
    if (!this.startTime) {
      return 0;
    }
  
    return Date.now() - this.startTime;
  }

  start () {
    if (this.isRunning) {
      return console.error('Timer is already running');
    }

    this.isRunning = true;

    this.startTime = Date.now();
  }

  stop () {
    if (!this.isRunning) {
      return console.error('Timer is already stopped');
    }

    this.isRunning = false;

    this.overallTime = this.overallTime + this._getTimeElapsedSinceLastStart();
  }

  reset () {
    this.overallTime = 0;

    if (this.isRunning) {
      this.startTime = Date.now();
      return;
    }

    this.startTime = 0;
  }

  getTime () {
    if (!this.startTime) {
      return 0;
    }

    if (this.isRunning) {
      return this.overallTime + this._getTimeElapsedSinceLastStart();
    }

    return this.overallTime;
  }
}

const timer = new Timer();
timer.start();
setInterval(() => {
  const timeInSeconds = Math.round(timer.getTime() / 1000);
  document.getElementById('time').innerText = timeInSeconds;
}, 100)
<p>Elapsed time: <span id="time">0</span>s</p>

该代码段还包括针对您的问题的解决方案。因此seconds,我们不是每 1000 毫秒间隔递增变量,而是启动计时器,然后每 100 毫秒*我们只是从计时器中读取经过的时间并相应地更新视图。

* - 使其比 1000 毫秒更准确

为了使您的计时器更准确,您必须四舍五入

我同意 Bergi 使用 Date 的观点,但他的解决方案对我来说有点矫枉过正。我只是想让我的动画时钟(数字和模拟 SVG)在第二个更新,而不是超限或欠运行,从而在时钟更新中产生明显的跳跃。这是我放在时钟更新函数中的代码片段:

    var milliseconds = now.getMilliseconds();
    var newTimeout = 1000 - milliseconds;
    this.timeoutVariable = setTimeout((function(thisObj) { return function() { thisObj.update(); } })(this), newTimeout);

它只是计算下一个偶数秒的增量时间,并将超时设置为该增量。这会将我所有的时钟对象同步到第二个。希望这是有帮助的。