web-dev-qa-db-fra.com

Comment créer une minuterie précise en javascript?

Je dois créer une minuterie simple mais précise.

Ceci est mon code:

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

Après exactement 3600 secondes, il imprime environ 3500 secondes.

  • Pourquoi n'est-ce pas exact?

  • Comment créer un chronomètre précis?

23
xRobot

Pourquoi n'est-ce pas exact?

Parce que vous utilisez setTimeout() ou setInterval(). On ne peut pas leur faire confiance , il n’existe aucune garantie d’exactitude. Ils sont autorisés à traîner arbitrairement, et ils ne suivent pas un rythme constant mais ont tendance à dériver (comme vous l'avez observé).

Comment créer un chronomètre précis?

Utilisez plutôt l'objet Date pour obtenir l'heure exacte (millisecondes) exacte. Ensuite, basez votre logique sur la valeur temporelle actuelle au lieu de compter le nombre de fois que votre rappel a été exécuté.

Pour un simple chronomètre ou une horloge, gardez une trace de l'heure difference explicitement:

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

Maintenant, cela pose le problème de la possibilité de sauter des valeurs. Lorsque l'intervalle se décale un peu et exécute votre rappel après 990, 1993, 2996, 3999, 5002 millisecondes, vous verrez le deuxième compte 0, 1, 2, 3, 5 (!). Il serait donc conseillé de mettre à jour plus souvent, environ toutes les 100 ms, pour éviter de tels sauts.

Cependant, vous avez parfois besoin d'un intervalle régulier pour exécuter vos rappels sans dérive. Cela nécessite un peu plus de stratégie (et de code) plus avantageuse, bien que cela paye bien (et enregistre moins de délais). Celles-ci sont appelées minuteries auto-ajustables. Ici, le délai exact pour chacun des délais répétés est adapté au temps réellement écoulé, par rapport aux intervalles prévus:

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
}
78
Bergi

Je construis juste sur la réponse de Bergi (spécifiquement la deuxième partie) un peu parce que j’ai vraiment aimé la façon dont cela a été fait, mais je veux l’option pour arrêter le chronomètre une fois qu’il démarre (comme clearInterval() presque). Sooo ... Je l'ai emballé dans une fonction constructeur afin que nous puissions faire des choses "objectives" avec.

1. constructeur

Bon, alors vous copiez/collez ça ...

/**
 * 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. instancier

Dites-lui quoi faire et tout ça ...

// 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. Alors fais ... des trucs

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

Je veux dire, ça marche pour moi quand même. S'il y a un meilleur moyen, je vous préviens.

9
Leon Williams

Je suis d'accord avec Bergi sur l'utilisation de Date, mais sa solution était un peu excessive pour mon utilisation. Je voulais simplement que mon horloge animée (SVG numériques et analogiques) se mette à jour à la seconde, mais ne soit ni dépassée ni dépassée, créant des sauts évidents dans les mises à jour de l'horloge. Voici l'extrait de code que j'ai mis dans mes fonctions de mise à jour de l'horloge:

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

Il calcule simplement le temps delta à la seconde paire, et définit le délai d’attente sur ce delta. Cela synchronise tous mes objets d'horloge à la seconde. J'espère que c'est utile.

3
agent-p

Ne devient pas beaucoup plus précis que cela.

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

Quant à savoir pourquoi cela n’est pas précis, je suppose que la machine est occupée à d’autres tâches, ralentir un peu à chaque itération s’ajoute, comme vous le voyez.

3
php_nub_qq

C'est une vieille question, mais je pensais partager le code que j'utilise parfois:

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

Exemple:

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

Corrige elle-même la setTimeout, peut l'exécuter X fois (-1 pour l'infini), peut commencer à s'exécuter instantanément et dispose d'un compteur si vous avez besoin de voir combien de fois la func() a été exécutée. Est très pratique.

Edit: Notez que ceci ne fait aucune vérification des entrées (comme si delay et repeat sont du type correct. Et vous voudrez probablement ajouter une sorte de fonction get/set si vous voulez obtenir le nombre ou changer la valeur de répétition .

1
V. Rubinetti