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

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

Это мой код:

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

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

  • почему это не точный ?

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

5 ответов


почему это не точный?

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

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

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


не получается намного точнее, чем это.

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

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


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

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

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


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

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

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