Как создать точный таймер в javascript?

Мне нужно создать простой, но точный таймер.

Это мой код:

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

Через ровно 3600 секунд он печатает около 3500 секунд.

  • Почему это не верно?

  • Как создать точный таймер?

Ответ 1

Почему это не точно?

Потому что вы используете 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
}

Ответ 2

Я просто опишу ответ Берги (в частности, вторую часть) немного, потому что мне очень понравилось, как это было сделано, но я хочу, чтобы остановите таймер после его запуска (например, 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) - 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

Расскажите, что делать и что...

// 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 in progress
ticker.interval = 99;

Я имею в виду, это работает для меня в любом случае. Если есть лучший способ, знаете ли.

Ответ 3

Я согласен с Берги в использовании Date, но его решение было немного излишним для моего использования. Я просто хотел, чтобы мои анимированные часы (цифровые и аналоговые SVG) обновлялись на втором и не переполнялись или выполнялись, создавая очевидные скачки в обновлениях часов. Вот фрагмент кода, который я ввел в мои функции обновления часов:

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

Он просто вычисляет дельта-время на следующую четную секунду и устанавливает тайм-аут на эту дельта. Это синхронизирует все мои объекты часов со вторым. Надеюсь, что это будет полезно.

Ответ 4

Большинство таймеров в ответах здесь будут отставать от ожидаемого времени, поскольку они устанавливают "ожидаемое" значение на идеальное и учитывают только задержку, которую браузер ввел до этого момента. Это хорошо, если вам просто нужны точные интервалы, но если вы рассчитываете время относительно других событий, то у вас (почти) всегда будет такая задержка.

Чтобы исправить это, вы можете отслеживать историю дрейфа и использовать его для прогнозирования будущего дрейфа. Добавляя вторичную корректировку с этой упреждающей коррекцией, дисперсия дрейфа центрируется вокруг целевого времени. Например, если вы всегда получаете отклонение от 20 до 40 мс, эта корректировка сместит его к -10 на +10ms примерно за целевое время.

Основываясь на ответе Берги, я использовал скользящую медиану для своего алгоритма прогнозирования. Взятие всего 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 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));
}

Ответ 5

Не намного точнее, чем это.

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

Что касается того, почему это неточно, я бы предположил, что машина занята другими делами, медленное замедление на каждой итерации складывается, как вы видите.

Ответ 6

Это старый вопрос, но я решил поделиться кодом, который иногда использую:

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() был запустить. Пригодится.

Изменение: Обратите внимание, что это не делает никакой проверки ввода (например, если задержка и повтор являются правильными типами. И вы, вероятно, захотите добавить какую-то функцию get/set, если вы хотите получить счетчик или изменить значение повторения.

Ответ 7

Берги отвечает точно, почему таймер из вопроса не является точным. Вот мой простой таймер JS с методами start, stop, reset и getTime:

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>