web-dev-qa-db-fra.com

Existe-t-il un moyen plus précis de créer un minuteur Javascript que setTimeout?

Ce qui m’a toujours posé problème, c’est à quel point la méthode setTimeout() en Javascript est imprévisible.

D'après mon expérience, la minuterie est horriblement inexacte dans de nombreuses situations. Par inexact, je veux dire que le temps de retard réel semble varier de 250 à 500 ms plus ou moins. Bien que le temps ne soit pas énorme, lorsque vous l'utilisez pour masquer/afficher des éléments de l'interface utilisateur, le temps peut être visiblement perceptible.

Existe-t-il des astuces qui permettent de s'assurer que setTimeout() fonctionne correctement (sans recourir à une API externe) ou s'agit-il d'une cause perdue?

65
Dan Herbert

Y at-il des astuces qui peuvent être faites pour s'assurer que setTimeout() effectue avec précision (sans recourir à une API externe .__) ou s'agit-il d'une cause perdue?

Non et non Avec setTimeout(), vous n'allez pas vous approcher d'une minuterie parfaitement précise - les navigateurs ne sont pas configurés pour cela. Cependant , vous n'avez pas besoin de compter dessus pour chronométrer les choses non plus. La plupart des bibliothèques d'animation ont compris cela il y a des années: vous avez configuré un rappel avec setTimeout(), mais vous déterminez ce qui doit être fait en fonction de la valeur de (new Date()).milliseconds (ou son équivalent). Cela vous permet de tirer parti d'une prise en charge de la minuterie plus fiable dans les nouveaux navigateurs, tout en conservant un comportement approprié sur les anciens navigateurs.

Cela vous permet également de éviter d'utiliser trop de temporisateurs! Ceci est important: chaque minuterie est un rappel. Chaque rappel exécute le code JS. Pendant l'exécution du code JS, les événements du navigateur, y compris d'autres rappels, sont retardés ou supprimés. Lorsque le rappel est terminé, des rappels supplémentaires doivent concurrencer d'autres événements du navigateur pour avoir une chance de s'exécuter. Par conséquent, un minuteur qui gère toutes les tâches en attente pour cet intervalle fonctionnera mieux que deux minuteurs avec des intervalles identiques, et (pour des délais plus courts) mieux que deux minuteurs avec des délais se chevauchant!

Résumé: arrêtez d'utiliser setTimeout() pour implémenter des conceptions "un minuteur/une tâche", et utilisez l'horloge en temps réel pour lisser les actions de l'interface utilisateur. 

71
Shog9

.

REF; http://www.sitepoint.com/creating-accurate-timers-in-javascript/

Ce site m'a sauvé sur une échelle majeure.

Vous pouvez utiliser l'horloge système pour compenser l'inexactitude du chronomètre. Si vous exécutez une fonction de minutage sous la forme d'une série d'appels setTimeout (chaque instance appelant la suivante), il vous suffira alors de déterminer exactement son inexactitude et de soustraire cette différence de la prochaine itération:

var start = new Date().getTime(),  
    time = 0,  
    elapsed = '0.0';  
function instance()  
{  
    time += 100;  
    elapsed = Math.floor(time / 100) / 10;  
    if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }  
    document.title = elapsed;  
    var diff = (new Date().getTime() - start) - time;  
    window.setTimeout(instance, (100 - diff));  
}  
window.setTimeout(instance, 100);  

Cette méthode minimisera la dérive et réduira les imprécisions de plus de 90%.

Il a corrigé mes problèmes, espérons que cela aide 

32
user1213320

J'ai eu un problème similaire il n'y a pas si longtemps et j'ai mis au point une approche combinant requestAnimationFrame et performance.now (), qui fonctionne très efficacement.

Je suis maintenant capable de faire des minuteries précises à environ 12 décimales:

    window.performance = window.performance || {};
    performance.now = (function() {
        return performance.now       ||
            performance.mozNow    ||
            performance.msNow     ||
            performance.oNow      ||
            performance.webkitNow ||
                function() {
                    //Doh! Crap browser!
                    return new Date().getTime(); 
                };
        })();

http://jsfiddle.net/CGWGreen/9pg9L/

10
Chris GW Green

la réponse de shog9 est à peu près ce que je dirais, bien que j'ajouterais ce qui suit à propos de l'animation/des événements d'interface utilisateur:

Si vous avez une boîte qui est supposée glisser sur l’écran, s’étendre vers le bas, puis fondre dans son contenu, n’essayez pas de séparer les trois événements avec des retards programmés pour les déclencher une à une. le premier événement est terminé, il appelle le expandeur, puis le fader. jQuery peut le faire facilement, et je suis sûr que d’autres bibliothèques le peuvent aussi.

4
matt lohkamp

Si vous devez obtenir un rappel précis sur un intervalle donné, cet article peut vous aider:

https://Gist.github.com/1185904

4
manast

Si vous utilisez setTimeout() pour céder rapidement le navigateur afin que son thread d'interface utilisateur puisse reprendre toutes les tâches nécessaires (telles que la mise à jour d'un onglet ou la non-présentation de la boîte de dialogue Script à exécution longue), une nouvelle API appelée Efficient Script Yielding , aka, setImmediate(), qui fonctionnera peut-être un peu mieux pour vous.

setImmediate() fonctionne de manière très similaire à setTimeout(), mais il peut fonctionner immédiatement si le navigateur n’a rien d’autre à faire. Dans de nombreuses situations où vous utilisez setTimeout(..., 16) ou setTimeout(..., 4) ou setTimeout(..., 0) (c'est-à-dire que vous voulez que le navigateur exécute les tâches de thread d'interface utilisateur en attente sans afficher de dialogue de script à exécution longue), vous pouvez simplement remplacer votre setTimeout() par setImmediate(), en supprimant la seconde (milliseconde) argument.

La différence avec setImmediate() est qu’il s’agit essentiellement d’un rendement; si le navigateur doit parfois effectuer des tâches sur le fil de l'interface utilisateur (par exemple, mettre à jour un onglet), il le fera avant de revenir à votre rappel. Cependant, si le navigateur est déjà entièrement rattrapé par son travail, le rappel spécifié dans setImmediate() sera essentiellement exécuté sans délai.

Malheureusement, il n’est actuellement supporté que dans IE9 +, car il existe des Push back des autres éditeurs de navigateurs.

Il y a un bon polyfill disponible si vous voulez l'utiliser et espérez que les autres navigateurs l'implémenteront à un moment donné.

Si vous utilisez setTimeout() pour l'animation, requestAnimationFrame est votre meilleur choix car votre code s'exécutera en synchronisation avec le taux de rafraîchissement du moniteur.

Si vous utilisez setTimeout() sur une cadence plus lente, par exemple. une fois toutes les 300 millisecondes, vous pouvez utiliser une solution similaire à celle suggérée par user1213320, qui vous permet de contrôler le temps écoulé depuis le dernier horodatage de votre minuterie et de compenser tout retard. Une amélioration est que vous pouvez utiliser le nouveau Temps de résolution élevée interface (aussi appelé window.performance.now()) au lieu de Date.now() pour obtenir une résolution supérieure à la milliseconde pour l'heure actuelle.

3
NicJ

Vous devez "ramper" sur l'heure cible. Quelques essais et erreurs seront nécessaires mais en substance.

Définir un délai d'attente pour terminer environ 100 ms avant l'heure requise

faites en sorte que le gestionnaire de délai d'attente fonctionne comme ceci:

calculate_remaining_time
if remaining_time > 20ms // maybe as much as 50
  re-queue the handler for 10ms time
else
{
  while( remaining_time > 0 ) calculate_remaining_time;
  do_your_thing();
  re-queue the handler for 100ms before the next required time
}

Mais votre boucle while peut toujours être interrompue par d'autres processus, elle n'est donc pas parfaite.

2
Noel Walters

Voici un exemple de démonstration de la suggestion de Shog9. Ceci remplit une barre de progression jQuery en 6 secondes, puis redirige vers une autre page une fois remplie: 

var TOTAL_SEC = 6;
var FRAMES_PER_SEC = 60;
var percent = 0;
var startTime = new Date().getTime();

setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);

function updateProgress() {
    var currentTime = new Date().getTime();

    // 1000 to convert to milliseconds, and 100 to convert to percentage
    percent = (currentTime - startTime) / (TOTAL_SEC * 1000) * 100;

    $("#progressbar").progressbar({ value: percent });

    if (percent >= 100) {
        window.location = "newLocation.html";
    } else {
        setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);
    }                 
}
2
Dean

C’est une minuterie que j’ai faite pour un de mes projets musicaux qui fait cette chose. Minuterie précise sur tous les appareils.

var Timer = function(){
  var framebuffer = 0,
  var msSinceInitialized = 0,
  var timer = this;

  var timeAtLastInterval = new Date().getTime();

  setInterval(function(){
    var frametime = new Date().getTime();
    var timeElapsed = frametime - timeAtLastInterval;
    msSinceInitialized += timeElapsed;
    timeAtLastInterval = frametime;
  },1);

  this.setInterval = function(callback,timeout,arguments) {
    var timeStarted = msSinceInitialized;
    var interval = setInterval(function(){
      var totaltimepassed = msSinceInitialized - timeStarted;
      if (totaltimepassed >= timeout) {
        callback(arguments);
        timeStarted = msSinceInitialized;
      }
    },1);

    return interval;
  }
}

var timer = new Timer();
timer.setInterval(function(){console.log("This timer will not drift."),1000}

1
Code Whisperer

Les délais d'attente de JavaScript ont une limite de facto de 10-15 ms (je ne suis pas sûr de ce que vous faites pour obtenir 200 ms, à moins que vous ne fassiez 185 ms d'exécution réelle). Cela est dû au fait que les fenêtres ont une résolution standard de 15 ms. La seule façon de faire mieux consiste à utiliser les minuteries à résolution plus élevée de Windows, ce qui permet au système de fonctionner avec d’autres applications du système et de mâcher la vie de la batterie un bug d’Intel sur cette question).

La norme de facto de 10-15 ms est due au fait que les personnes utilisent des délais d'expiration de 0 ms sur des sites Web, mais codent ensuite de manière à supposer un délai d'expiration de 10-15ms jeu/site/animation va de quelques ordres de grandeur plus rapidement que prévu). Pour tenir compte de cela, même sur les plates-formes sans problèmes de minuterie de Windows, les navigateurs limitent la résolution de la minuterie à 10 ms. 

0
olliej

Voici ce que j'utilise. Puisqu'il s'agit de JavaScript, je publierai mes solutions Frontend et node.js:

Pour les deux, j'utilise la même fonction d'arrondi décimal que je vous recommande vivement de garder à bout de bras, pour les raisons suivantes:

const round = (places, number) => +(Math.round(number + `e+${places}`) + `e-${places}`)

places - Nombre de décimales à arrondir. Ceci devrait être sûr et éviter tout problème avec les flottants (des nombres comme 1.0000000000005 ~ peuvent être problématiques). I J'ai passé du temps à chercher le meilleur moyen d'arrondir les décimales fournies par des minuteries haute résolution converties en millisecondes.

that + symbol - C'est un opérateur unaire qui convertit un opérande en un nombre pratiquement identique à Number()

Navigateur

const start = performance.now()

// I wonder how long this comment takes to parse

const end = performance.now()

const result = (end - start) + ' ms'

const adjusted = round(2, result) // see above rounding function

node.js

// Start timer
const startTimer = () => process.hrtime()

// End timer
const endTimer = (time) => {
    const diff = process.hrtime(time)
    const NS_PER_SEC = 1e9
    const result = (diff[0] * NS_PER_SEC + diff[1])
    const elapsed = Math.round((result * 0.0000010))
    return elapsed
}

// This end timer converts the number from nanoseconds into milliseconds;
// you can find the nanosecond version if you need some seriously high-resolution timers.

const start = startTimer()

// I wonder how long this comment takes to parse

const end = endTimer(start)

console.log(end + ' ms')
0
agm1984

Je déteste le dire, mais je ne pense pas qu'il y ait un moyen de remédier à la situation. Je pense cependant que cela dépend du système client, donc un moteur ou une machine javascript plus rapide peut le rendre légèrement plus précis.

0
Eric Wendelin

Selon mon expérience, c’est un effort perdu, même si la plus petite quantité de temps raisonnable dans laquelle j’ai jamais reconnu d’agir est d’environ 32 à 33 ms. ...

0
roenving

Il y a certainement une limite ici. Pour vous donner une idée, le navigateur Chrome que Google vient de publier est suffisamment rapide pour pouvoir exécuter setTimeout (function () {}, 0) en 15 à 20 ms, alors que les moteurs JavaScript plus anciens mettaient des centaines de millisecondes à exécuter cette fonction. Bien que setTimeout utilise des millisecondes, aucune machine virtuelle javascript à ce stade ne peut exécuter le code avec cette précision.

0
Bialecki

Dan, grâce à mon expérience (qui inclut l’implémentation du langage SMIL2.1 en JavaScript, où la gestion du temps est un sujet), je peux vous assurer que vous n’avez jamais besoin de la haute précision de setTimeout ou de setInterval.

Cependant, ce qui importe, c'est l'ordre dans lequel setTimeout/setInterval est exécuté lorsqu'il est mis en file d'attente - et cela fonctionne toujours parfaitement.

0
Sergey Ilinsky