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

How to create an accurate timer in javascript?

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

这是我的代码:

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

恰好 3600 秒后,打印大约 3500 秒。

没有比这更准确的了。

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

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

Why is it not accurate?

因为您正在使用 setTimeout()setInterval()They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lag arbitrarily, and they do not keep a constant pace but tend to drift(如您所见)。

How can I create an accurate timer?

改为使用 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
}

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

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

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

我只是在 (特别是第二部分)的基础上做了一点改进,因为我真的很喜欢它的完成方式,但我希望可以选择在计时器启动后停止它(比如 clearInterval() 差不多)。 Sooo...我已经将它包装到一个构造函数中,所以我们可以用它做 'objecty' 事情。

1。构造函数

好吧,所以你 copy/paste 那...

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

我的意思是,无论如何它对我有用。如果有更好的方法,请告诉我。

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

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 表示无限次),可以立即开始 运行ning,并且有一个计数器,如果你曾经需要查看 func() 已 运行 的次数。派上用场。

编辑:请注意,这不会进行任何输入检查(比如延迟和重复是否是正确的类型。如果您想获得,您可能想要添加某种 get/set 函数计数或更改重复值。

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

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

的基础上,我为我的预测算法使用了滚动中位数。用这种方法只取 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 的回答准确地指出了问题中的计时器不准确的原因。这是我对带有 startstopresetgetTime 方法的简单 JS 计时器的看法:

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>

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

* - 使其比 1000 毫秒更准确

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

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

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

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

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)

这是一个解决方案,当 window 被隐藏时暂停,并且可以使用中止控制器取消。

function animationInterval(ms, signal, callback) {
  const start = document.timeline.currentTime;

  function frame(time) {
    if (signal.aborted) return;
    callback(time);
    scheduleFrame(time);
  }

  function scheduleFrame(time) {
    const elapsed = time - start;
    const roundedElapsed = Math.round(elapsed / ms) * ms;
    const targetNext = start + roundedElapsed + ms;
    const delay = targetNext - performance.now();
    setTimeout(() => requestAnimationFrame(frame), delay);
  }

  scheduleFrame(start);
}

用法:

const controller = new AbortController();

// Create an animation callback every second:
animationInterval(1000, controller.signal, time => {
  console.log('tick!', time);
});

// And stop it sometime later:
controller.abort();

driftless 是 setInterval 的替代品,可减轻漂移。让生活变得简单,导入 npm 包,然后像 setInterval / setTimeout 一样使用它:

setDriftlessInterval(() => {
    this.counter--;
}, 1000);

setDriftlessInterval(() => {
    this.refreshBounds();
}, 20000);

您可以使用一个名为 setTimeout 的函数来设置倒计时。

首先,创建一个 javascript 片段并将其添加到您的页面,如下所示;

var remainingTime = 30;
    var elem = document.getElementById('countdown_div');
    var timer = setInterval(countdown, 1000); //set the countdown to every second
    function countdown() {
      if (remainingTime == -1) {
        clearTimeout(timer);
        doSomething();
      } else {
        elem.innerHTML = remainingTime + ' left';
        remainingTime--; //we subtract the second each iteration
      }
    }

来源 + 更多详细信息 -> https://www.growthsnippets.com/30-second-countdown-timer-javascript/

这里的许多答案都很棒,但它们的代码示例通常是一页又一页的代码(好的答案甚至有关于 copy/paste 这一切的最佳方法的说明)。我只是想通过一个非常简单的例子来理解这个问题。

工作演示

var lastpause = 0;
var totaltime = 0;

function goFunction(e) {
    if(this.innerText == 'Off - Timer Not Going') {
        this.innerText = 'On - Timer Going';
    } else {
        totaltime += Date.now() - lastpause;
        this.innerText = 'Off - Timer Not Going';
    }
    lastpause = Date.now();
    document.getElementById('count').innerText = totaltime + ' milliseconds.';
}

document.getElementById('button').addEventListener('click', goFunction);
<button id="button">Off - Timer Not Going</button> <br>
Seconds: <span id="count">0 milliseconds.</span>

演示说明

  • totaltime — 这是计算的总时间。
  • lastpause — 这是我们拥有的唯一真正的临时变量。每当有人点击暂停时,我们将 lastpause 设置为 Date.now()。当有人取消暂停并再次暂停时,我们计算从上次暂停中减去 Date.now() 的时间差。

我们只需要这两个变量:我们的总数和我们上次停止计时器的时间。其他答案好像都是用这个方法,但我想要一个简洁的解释。

现代的、完全可编程的定时器

此计时器采用以赫兹为单位的频率,以及最多可采用四个参数的回调、当前帧索引、当前时间、当前帧理想发生的时间以及对计时器实例(因此调用者和回调都可以访问它的方法)。

注意:所有时间均基于 performance.now,并且相对于页面加载的时间。

计时器实例具有三个 API 方法:

  • stop:不带参数。立即(并永久)终止计时器。 Returns 下一帧(取消的帧)的帧索引。
  • adapt:获取以赫兹为单位的频率并使计时器适应它,开始 从下一帧开始。 Returns 以毫秒为单位的隐含间隔。
  • redefine:接受一个新的回调函数。与当前交换它 打回来。影响下一帧。 Returns undefined.

注意:tick 方法显式传递 this(作为 self)以解决 this 引用 window 的问题,当 tick 方法通过 setTimeout.

调用
class ProgrammableTimer {

    constructor(hertz, callback) {

        this.target = performance.now();     // target time for the next frame
        this.interval = 1 / hertz * 1000;    // the milliseconds between ticks
        this.callback = callback;
        this.stopped = false;
        this.frame = 0;

        this.tick(this);
    }

    tick(self) {

        if (self.stopped) return;

        const currentTime = performance.now();
        const currentTarget = self.target;
        const currentInterval = (self.target += self.interval) - currentTime;

        setTimeout(self.tick, currentInterval, self);
        self.callback(self.frame++, currentTime, currentTarget, self);
    }

    stop() { this.stopped = true; return this.frame }

    adapt(hertz) { return this.interval = 1 / hertz * 1000 }

    redefine(replacement) { this.callback = replacement }
}