web-dev-qa-db-ja.com

JavaScriptで正確なタイマーを作成する方法は?

シンプルだが正確なタイマーを作成する必要があります。

これは私のコードです:

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

正確に3600秒後に、約3500秒が印刷されます。

  • なぜ正確ではないのですか?

  • 正確なタイマーを作成するにはどうすればよいですか?

30
xRobot

なぜ正確ではないのですか?

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

さて、これには値がジャンプする可能性があるという問題があります。間隔が少し遅れて、9901993299639995002ミリ秒後にコールバックを実行すると、2番目のカウント01235(!)。そのため、このようなジャンプを回避するために、約100ミリ秒ごとなど、より頻繁に更新することをお勧めします。

ただし、ドリフトせずにコールバックを実行する安定した間隔が本当に必要な場合があります。これには、より有利な戦略(およびコード)が必要です(ただし、タイムアウトの登録は少なくなります)。それらはself-adjustingタイマーとして知られています。ここでは、予想される間隔と比較して、繰り返されるタイムアウトのそれぞれの正確な遅延が実際の経過時間に適合されます。

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

Dateの使用についてはBergiに同意しますが、彼の解決策は私の使用には少し過剰でした。アニメーションクロック(デジタルおよびアナログSVG)が2番目に更新され、オーバーランまたはアンダーランではなく、クロック更新に明らかなジャンプが発生するようにしたかっただけです。クロック更新機能に追加したコードの抜粋を以下に示します。

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

次の偶数秒までのデルタ時間を計算し、そのデルタにタイムアウトを設定します。これにより、すべての時計オブジェクトが秒に同期されます。これが役に立てば幸いです。

3
agent-p

ここでの回答のほとんどのタイマーは、「期待」値を理想値に設定し、その時点までにブラウザーが導入した遅延のみを考慮しているため、予想時間よりも遅れます。正確な間隔だけが必要な場合はこれで問題ありませんが、他のイベントに関連するタイミングの場合は、(ほぼ)常にこの遅延が発生します。

これを修正するには、ドリフト履歴を追跡し、それを使用して将来のドリフトを予測できます。このプリエンプティブな補正で二次調整を追加することにより、ドリフトの分散は目標時間を中心に集中します。たとえば、常に20〜40ミリ秒のドリフトが発生する場合、この調整により、ターゲット時間の前後で-10〜+10ミリ秒にシフトします。

Bergi's answer に基づいて、予測アルゴリズムにローリング中央値を使用しました。この方法でサンプルを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));
}
3
Blorf

これよりはるかに正確にはなりません。

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

なぜそれが正確ではないのかについては、マシンが他のことをするのに忙しく、繰り返しのたびに少し遅くなると思います。

1
php_nub_qq

これは古い質問ですが、時々使用するコードをいくつか共有すると思いました。

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関数を追加したいでしょう。 。

1
V. Rubinetti