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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29971898/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 04:24:54  来源:igfitidea点击:

How to create an accurate timer in javascript?

javascripttimesetinterval

提问by xRobot

I need to create a simple but accurate timer.

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

This is my code:

这是我的代码:

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

After exactly 3600 seconds, it prints about 3500 seconds.

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

  • Why is it not accurate?

  • How can I create an accurate timer?

  • 为什么不准确?

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

回答by Bergi

Why is it not accurate?

为什么不准确?

Because you are using setTimeout()or setInterval(). They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lagarbitrarily, and they do not keep a constant pace but tend to drift(as you have observed).

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

How can I create an accurate timer?

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

Use the Dateobject instead to get the (millisecond-)accurate, current time. Then base your logic on the current time value, instead of counting how often your callback has been executed.

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

For a simple timer or clock, keep track of the time differenceexplicitly:

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

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

Now, that has the problem of possibly jumping values. When the interval lags a bit and executes your callback after 990, 1993, 2996, 3999, 5002milliseconds, you will see the second count 0, 1, 2, 3, 5(!). So it would be advisable to update more often, like about every 100ms, to avoid such jumps.

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

However, sometimes you really need a steady interval executing your callbacks without drifting. This requires a bit more advantaged strategy (and code), though it pays out well (and registers less timeouts). Those are known as self-adjustingtimers. Here the exact delay for each of the repeated timeouts is adapted to the actually elapsed time, compared to the expected intervals:

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

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
}

回答by Leon Williams

I'ma just build on Bergi's answer(specifically the second part) a little bit because I really liked the way it was done, but I want the option to stop the timer once it starts (like clearInterval()almost). Sooo... I've wrapped it up into a constructor function so we can do 'objecty' things with it.

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

1. Constructor

1. 构造函数

Alright, so you copy/paste that...

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

/**
 * 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) - This 
 * @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. Instantiate

2. 实例化

Tell it what to do and all that...

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

// 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. Then do... stuff

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;

I mean, it works for me anyway. If there's a better way, lemme know.

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

回答by Blorf

Most of the timers in the answers here will linger behind the expected time because they set the "expected" value to the ideal and only account for the delay that the browser introduced before that point. This is fine if you just need accurate intervals, but if you are timing relative to other events then you will (nearly) always have this delay.

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

To correct it, you can keep track of the drift history and use it to predict future drift. By adding a secondary adjustment with this preemptive correction, the variance in the drift centers around the target time. For example, if you're always getting a drift of 20 to 40ms, this adjustment would shift it to -10 to +10ms around the target time.

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

Building on Bergi's answer, I've used a rolling median for my prediction algorithm. Taking just 10 samples with this method makes a reasonable difference.

基于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));
}

回答by Tomasz Buba?a

Bergi's answer pinpoints exactly why the timer from the question is not accurate. Here's my take on a simple JS timer with start, stop, resetand getTimemethods:

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>

The snippet also includes a solution for your problem. So instead of incrementing secondsvariable every 1000ms interval, we just start the timer and then every 100ms* we just read elapsed time from the timer and update the view accordingly.

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

* - makes it more accurate than 1000ms

* - 使其比 1000 毫秒更准确

To make your timer more accurate, you would have to round

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

回答by agent-p

I agree with Bergi on using Date, but his solution was a bit of overkill for my use. I simply wanted my animated clock (digital and analog SVGs) to update on the second and not overrun or under run creating obvious jumps in the clock updates. Here is the snippet of code I put in my clock update functions:

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

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

It simply calculates the delta time to the next even second, and sets the timeout to that delta. This syncs all of my clock objects to the second. Hope this is helpful.

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

回答by php_nub_qq

Doesn't get much more accurate than this.

没有比这更准确的了。

var seconds = new Date().getTime(), last = seconds,

intrvl = setInterval(function() {
    var now = new Date().getTime();

    if(now - last > 5){
        if(confirm("Delay registered, terminate?")){
            clearInterval(intrvl);
            return;
        }
    }

    last = now;
    timer.innerHTML = now - seconds;

}, 333);

As to why it is not accurate, I would guess that the machine is busy doing other things, slowing down a little on each iteration adds up, as you see.

至于为什么它不准确,我猜想机器正忙于做其他事情,如您所见,每次迭代加起来都会放慢一点。

回答by V. Rubinetti

This is an old question but figured I'd share some code I use sometimes:

这是一个老问题,但我想我会分享一些我有时使用的代码:

function Timer(func, delay, repeat, runAtStart)
{
    this.func = func;
    this.delay = delay;
    this.repeat = repeat || 0;
    this.runAtStart = runAtStart;

    this.count = 0;
    this.startTime = performance.now();

    if (this.runAtStart)
        this.tick();
    else
    {
        var _this = this;
        this.timeout = window.setTimeout( function(){ _this.tick(); }, this.delay);
    }
}
Timer.prototype.tick = function()
{
    this.func();
    this.count++;

    if (this.repeat === -1 || (this.repeat > 0 && this.count < this.repeat) )
    {
        var adjustedDelay = Math.max( 1, this.startTime + ( (this.count+(this.runAtStart ? 2 : 1)) * this.delay ) - performance.now() );
        var _this = this;
        this.timeout = window.setTimeout( function(){ _this.tick(); }, adjustedDelay);
    }
}
Timer.prototype.stop = function()
{
    window.clearTimeout(this.timeout);
}

Example:

例子:

time = 0;
this.gameTimer = new Timer( function() { time++; }, 1000, -1);

Self-corrects the setTimeout, can run it X number of times (-1 for infinite), can start running instantaneously, and has a counter if you ever need to see how many times the func()has been run. Comes in handy.

自我修正setTimeout,可以运行 X 次(-1 表示无限),可以立即开始运行,如果您需要查看func()已运行了多少次,还可以使用计数器。派上用场。

Edit: Note, this doesn't do any input checking (like if delay and repeat are the correct type. And you'd probably want to add some kind of get/set function if you wanted to get the count or change the repeat value.

编辑:注意,这不做任何输入检查(比如延迟和重复是否是正确的类型。如果你想获得计数或改变重复值,你可能想要添加某种获取/设置函数.

回答by illusionist

One of my simplest implementations is down below. It can even survive page reloads. :-

我最简单的实现之一在下面。它甚至可以在页面重新加载后存活下来。:-

Code pen: https://codepen.io/shivabhusal/pen/abvmgaV

代码笔:https: //codepen.io/shivabhusal/pen/abvmgaV

$(function() {
  var TTimer = {
    startedTime: new Date(),
    restoredFromSession: false,
    started: false,
    minutes: 0,
    seconds: 0,
    
    tick: function tick() {
      // Since setInterval is not reliable in inactive windows/tabs we are using date diff.
      var diffInSeconds = Math.floor((new Date() - this.startedTime) / 1000);
      this.minutes = Math.floor(diffInSeconds / 60);
      this.seconds = diffInSeconds - this.minutes * 60;
      this.render();
      this.updateSession();
    },
    
    utilities: {
      pad: function pad(number) {
        return number < 10 ? '0' + number : number;
      }
    },
    
    container: function container() {
      return $(document);
    },
    
    render: function render() {
      this.container().find('#timer-minutes').text(this.utilities.pad(this.minutes));
      this.container().find('#timer-seconds').text(this.utilities.pad(this.seconds));

    },
    
    updateSession: function updateSession() {
      sessionStorage.setItem('timerStartedTime', this.startedTime);
    },
    
    clearSession: function clearSession() {
      sessionStorage.removeItem('timerStartedTime');
    },
    
    restoreFromSession: function restoreFromSession() {
      // Using sessionsStorage to make the timer persistent
      if (typeof Storage == "undefined") {
        console.log('No sessionStorage Support');
        return;
      }

      if (sessionStorage.getItem('timerStartedTime') !== null) {
        this.restoredFromSession = true;
        this.startedTime = new Date(sessionStorage.getItem('timerStartedTime'));
      }
    },
    
    start: function start() {
      this.restoreFromSession();
      this.stop();
      this.started = true;
      this.tick();
      this.timerId = setInterval(this.tick.bind(this), 1000);
    },
    
    stop: function stop() {
      this.started = false;
      clearInterval(this.timerId);
      this.render();
    }
  };

  TTimer.start();

});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>

<h1>
  <span id="timer-minutes">00</span> :
  <span id="timer-seconds">00</span>

</h1>