如何在JavaScript中创建准确的计时器?


75

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

这是我的代码:

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

恰好在3600秒后,它将打印约3500秒。

  • 为什么不准确?

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


更改timer.innerHTML并不总是即时的。您如何衡量“正确的”时间?
cbr 2015年

您受浏览器(和主机操作系统)的支配。您可以使用setTimeout()代替setInterval(),跟踪时间漂移并相应地调整每个睡眠时间。
Pointy 2015年

您可能会尝试通过各种回调使用setTimeout而不是setInterval。也许另一种方法是使用time对象?也许渲染的时刻和当前时间之间的差异更为精确(不过不确定)。
briosheje 2015年

如果您不能使用该Date对象,则还可以依赖该setTimeout()功能,并time()每隔N秒通过ajax与服务器的更新进行重新同步。
安德列斯SK

1
有趣的是,更少的时间却没有更多。我完全可以理解,花费更长的时间,但是少花钱是很奇怪的。
Simon_Weaver

Answers:


141

为什么不准确?

因为您正在使用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

现在,这可能会出现值跳跃的问题。当间隔滞后一点,并执行后回调9901993299639995002毫秒,你会看到第二计01235(!)。因此,建议更频繁地进行更新(例如大约每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
}

4
对第二个计时器代码进行一些解释将非常有帮助
Hassaan

1
这在很大程度上取决于您的计时器在做什么。在大多数情况下,有比step一次执行多次更好的方法。如果您使用差异策略(而不是计数器),则无论计时器是否是自调节的,都应使用该策略,您只需执行一个步骤,它就会自动追赶。
Bergi'3

1
@ nem035哦,好了,还有很多我尚未回答的问题:-)
Bergi 16'Aug

1
@JoshuaMichaelCalafell取决于您的用例,当步骤严重延迟时该怎么办。如果你什么都不做,它试图通过射击尽可能多的步骤,需要尽快赶上,这实际上可能是有害的动画等,你可以这样做expected += dt(同expected = Date.now()),或者expected += interval * Math.floor(dt / interval)这将只是跳过错过的步骤-可能罚款一切与三角洲合作。如果您正在计算步骤数,则可能需要进行调整。
Bergi '18

1
如上所述的计时器几乎总是会比预期的时间滞后一小段时间。如果您只需要精确的时间间隔,这很好,但是如果您要相对于其他事件进行计时,那么您总是会遇到这种小的延迟。要更正此问题,您还可以跟踪漂移历史记录并添加辅助调整,以使漂移围绕目标时间居中。例如,如果您要获得0至30ms的漂移,则可以将其调整为-15至+ 15ms。我用滚动的中值来做到这一点,以解决随时间变化的问题。仅抽取10个样本就产生了合理的差异。
Blorf

22

我只是基于Bergi的答案(特别是第二部分),因为我真的很喜欢它的完成方式,但是我希望该选项一旦启动就停止计时器(clearInterval()几乎一样)。如此...我已经将其包装到构造函数中,以便我们可以用它进行“客观的”事情。

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) - 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.实例化

告诉它怎么做以及所有这些...

// 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;

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


如果您愿意使用RxJS,则可以使用Observable.timer()和各种运算符来实现类似的解决方案:stackblitz.com/edit/…–
tedw

9

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

要更正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用这种先发制人的校正添加辅助调整,漂移的方差集中在目标时间附近。例如,如果您总是得到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));
}


7

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

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

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


6

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,我们不用启动变量,而是每1000ms间隔增加变量,而是每100ms *从定时器中读取经过的时间并相应地更新视图。

*-比1000ms更准确

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


1

没有比这更准确的了。

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);

至于为什么它不准确,我猜想这台机器正忙于做其他事情,如您所见,每次迭代的速度都会有所降低。


1
当用户开始更改操作系统时间时,事情变得很有趣。
eithed

@eithedog并没有比这更好的了。实际上,请稍等!
php_nub_qq 2015年

除非您想暂停2秒,然后显示的数字从例如10跳到12,否则请更频繁地进行此更新(例如100毫秒而不是1000毫秒)
James

@James好,如果您的计算机死机了2秒钟,我看不到更频繁地更新将如何对您有所帮助。
php_nub_qq 2015年

1
我会删除警报,因为如果出于好奇,我正在使用计时器,那么我将继续并更改当前系统时间,因为这就像您是在间接告诉我“呵呵,如果执行此操作会发生什么? ?!?!!?!” imgs.xkcd.com/comics/the_difference.png
briosheje

1

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

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

例:

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

自校正setTimeout,可以运行X次(无限次数为-1),可以立即开始运行,如果需要查看func()运行了多少次,还可以拥有一个计数器。派上用场。

编辑:注意,这不会做任何输入检查(例如,如果delay和repeat是正确的类型。并且,如果您想获取计数或更改重复值,则可能要添加某种get / set函数。 。


我很好奇。为什么(this.count+(this.runAtStart ? 2 : 1)使用runAtStart = true似乎会使第一步花费太长的时间,如果不使用runAtStart = true,为什么也会使第一帧时间太长。?
贾斯汀·文森特

我想我真正要问的是,有什么理由不能仅仅是这样吗?var adjustedDelay = Math.max( 1, this.startTime + (this.count * this.delay ) - performance.now() );
贾斯汀·文森特

老实说我不记得了。这段代码在这一点上已经很老了,可能会被重写得更干净。只是想复制/粘贴共享以显示与其他答案不同的方式,它具有指定的run N times参数,我发现它非常有用(而且如果需要,您可以使用原型进行此操作)。
V. Rubinetti

0

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

代码笔: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>


0

这是简单计时器的基本实现。注意:.timer是要显示计时器的类,done函数是您的实现,它将在计时器完成后触发

function TIMER() {
        var fiveMinutes = 60 * 5,
        display = document.querySelector('.timer');

        startTimer(fiveMinutes, display);
    }
    var myInterval;
    function startTimer(duration, display) {
        var timer = duration, minutes, seconds;

        myInterval = setInterval(function () {
            minutes = parseInt(timer / 60, 10)
            seconds = parseInt(timer % 60, 10);

            minutes = minutes < 10 ? "0" + minutes : minutes;
            seconds = seconds < 10 ? "0" + seconds : seconds;

            display.textContent = minutes + ":" + seconds;

            if (--timer < 0) {
                clearInterval(myInterval);
                doneFunction();
            }
        }, 1000);
    }

0

Bergi的回答启发,我创建了以下完整的无漂移计时器。我想要的是一种设置计时器,停止计时器并简单地执行此操作的方法。

var perfectTimer = {                                                              // Set of functions designed to create nearly perfect timers that do not drift
    timers: {},                                                                     // An object of timers by ID
  nextID: 0,                                                                      // Next available timer reference ID
  set: (callback, interval) => {                                                  // Set a timer
    var expected = Date.now() + interval;                                         // Expected currect time when timeout fires
    var ID = perfectTimer.nextID++;                                               // Create reference to timer
    function step() {                                                             // Adjusts the timeout to account for any drift since last timeout
      callback();                                                                 // Call the callback
      var dt = Date.now() - expected;                                             // The drift (ms) (positive for overshooting) comparing the expected time to the current time
      expected += interval;                                                       // Set the next expected currect time when timeout fires
      perfectTimer.timers[ID] = setTimeout(step, Math.max(0, interval - dt));     // Take into account drift
    }
    perfectTimer.timers[ID] = setTimeout(step, interval);                         // Return reference to timer
    return ID;
  },
  clear: (ID) => {                                                                // Clear & delete a timer by ID reference
    if (perfectTimer.timers[ID] != undefined) {                                   // Preventing errors when trying to clear a timer that no longer exists
      console.log('clear timer:', ID);
      console.log('timers before:', perfectTimer.timers);
      clearTimeout(perfectTimer.timers[ID]);                                      // Clear timer
      delete perfectTimer.timers[ID];                                             // Delete timer reference
      console.log('timers after:', perfectTimer.timers);
    }
    }       
}




// Below are some tests
var timerOne = perfectTimer.set(() => {
    console.log(new Date().toString(), Date.now(), 'timerOne', timerOne);
}, 1000);
console.log(timerOne);
setTimeout(() => {
    perfectTimer.clear(timerOne);
}, 5000)

var timerTwo = perfectTimer.set(() => {
    console.log(new Date().toString(), Date.now(), 'timerTwo', timerTwo);
}, 1000);
console.log(timerTwo);

setTimeout(() => {
    perfectTimer.clear(timerTwo);
}, 8000)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.