How to create an accurate timer in javascript?

前端 未结 10 2122
一个人的身影
一个人的身影 2020-11-22 01:32

I need to create a simple but accurate timer.

This is my code:

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


        
相关标签:
10条回答
  • 2020-11-22 02:11

    Bergi's answer pinpoints exactly why the timer from the question is not accurate. Here's my take on a simple JS timer with start, stop, reset and getTime methods:

    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>

    The snippet also includes a solution for your problem. So instead of incrementing seconds variable every 1000ms interval, we just start the timer and then every 100ms* we just read elapsed time from the timer and update the view accordingly.

    * - makes it more accurate than 1000ms

    To make your timer more accurate, you would have to round

    0 讨论(0)
  • 2020-11-22 02:16

    Why is it not accurate?

    Because you are using setTimeout() or setInterval(). They cannot be trusted, there are no accuracy guarantees for them. They are allowed to lag arbitrarily, and they do not keep a constant pace but tend to drift (as you have observed).

    How can I create an accurate timer?

    Use the Date object instead to get the (millisecond-)accurate, current time. Then base your logic on the current time value, instead of counting how often your callback has been executed.

    For a simple timer or clock, keep track of the time difference explicitly:

    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
    

    Now, that has the problem of possibly jumping values. When the interval lags a bit and executes your callback after 990, 1993, 2996, 3999, 5002 milliseconds, you will see the second count 0, 1, 2, 3, 5 (!). So it would be advisable to update more often, like about every 100ms, to avoid such jumps.

    However, sometimes you really need a steady interval executing your callbacks without drifting. This requires a bit more advantaged strategy (and code), though it pays out well (and registers less timeouts). Those are known as self-adjusting timers. Here the exact delay for each of the repeated timeouts is adapted to the actually elapsed time, compared to the expected intervals:

    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
    }
    
    0 讨论(0)
  • 2020-11-22 02:16

    One of my simplest implementations is down below. It can even survive page reloads. :-

    Code pen: https://codepen.io/shivabhusal/pen/abvmgaV

    $(function() {
      var TTimer = {
        startedTime: new Date(),
        restoredFromSession: false,
        started: false,
        minutes: 0,
        seconds: 0,
        
        tick: function tick() {
          // Since setInterval is not reliable in inactive windows/tabs we are using date diff.
          var diffInSeconds = Math.floor((new Date() - this.startedTime) / 1000);
          this.minutes = Math.floor(diffInSeconds / 60);
          this.seconds = diffInSeconds - this.minutes * 60;
          this.render();
          this.updateSession();
        },
        
        utilities: {
          pad: function pad(number) {
            return number < 10 ? '0' + number : number;
          }
        },
        
        container: function container() {
          return $(document);
        },
        
        render: function render() {
          this.container().find('#timer-minutes').text(this.utilities.pad(this.minutes));
          this.container().find('#timer-seconds').text(this.utilities.pad(this.seconds));
    
        },
        
        updateSession: function updateSession() {
          sessionStorage.setItem('timerStartedTime', this.startedTime);
        },
        
        clearSession: function clearSession() {
          sessionStorage.removeItem('timerStartedTime');
        },
        
        restoreFromSession: function restoreFromSession() {
          // Using sessionsStorage to make the timer persistent
          if (typeof Storage == "undefined") {
            console.log('No sessionStorage Support');
            return;
          }
    
          if (sessionStorage.getItem('timerStartedTime') !== null) {
            this.restoredFromSession = true;
            this.startedTime = new Date(sessionStorage.getItem('timerStartedTime'));
          }
        },
        
        start: function start() {
          this.restoreFromSession();
          this.stop();
          this.started = true;
          this.tick();
          this.timerId = setInterval(this.tick.bind(this), 1000);
        },
        
        stop: function stop() {
          this.started = false;
          clearInterval(this.timerId);
          this.render();
        }
      };
    
      TTimer.start();
    
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    
    <h1>
      <span id="timer-minutes">00</span> :
      <span id="timer-seconds">00</span>
    
    </h1>

    0 讨论(0)
  • 2020-11-22 02:19

    Most of the timers in the answers here will linger behind the expected time because they set the "expected" value to the ideal and only account for the delay that the browser introduced before that point. This is fine if you just need accurate intervals, but if you are timing relative to other events then you will (nearly) always have this delay.

    To correct it, you can keep track of the drift history and use it to predict future drift. By adding a secondary adjustment with this preemptive correction, the variance in the drift centers around the target time. For example, if you're always getting a drift of 20 to 40ms, this adjustment would shift it to -10 to +10ms around the target time.

    Building on Bergi's answer, I've used a rolling median for my prediction algorithm. Taking just 10 samples with this method makes a reasonable difference.

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

    0 讨论(0)
  • 2020-11-22 02:24

    I'ma just build on Bergi's answer (specifically the second part) a little bit because I really liked the way it was done, but I want the option to stop the timer once it starts (like clearInterval() almost). Sooo... I've wrapped it up into a constructor function so we can do 'objecty' things with it.

    1. Constructor

    Alright, so you copy/paste that...

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

    Tell it what to do and all that...

    // 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. Then do... stuff

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

    I mean, it works for me anyway. If there's a better way, lemme know.

    0 讨论(0)
  • 2020-11-22 02:30

    Inspired by Bergi's answer I created the following complete non drifting timer. What I wanted was a way to set a timer, stop it, and do this simply.

    var perfectTimer = {                                                              // Set of functions designed to create nearly perfect timers that do not drift
        timers: {},                                                                     // An object of timers by ID
      nextID: 0,                                                                      // Next available timer reference ID
      set: (callback, interval) => {                                                  // Set a timer
        var expected = Date.now() + interval;                                         // Expected currect time when timeout fires
        var ID = perfectTimer.nextID++;                                               // Create reference to timer
        function step() {                                                             // Adjusts the timeout to account for any drift since last timeout
          callback();                                                                 // Call the callback
          var dt = Date.now() - expected;                                             // The drift (ms) (positive for overshooting) comparing the expected time to the current time
          expected += interval;                                                       // Set the next expected currect time when timeout fires
          perfectTimer.timers[ID] = setTimeout(step, Math.max(0, interval - dt));     // Take into account drift
        }
        perfectTimer.timers[ID] = setTimeout(step, interval);                         // Return reference to timer
        return ID;
      },
      clear: (ID) => {                                                                // Clear & delete a timer by ID reference
        if (perfectTimer.timers[ID] != undefined) {                                   // Preventing errors when trying to clear a timer that no longer exists
          console.log('clear timer:', ID);
          console.log('timers before:', perfectTimer.timers);
          clearTimeout(perfectTimer.timers[ID]);                                      // Clear timer
          delete perfectTimer.timers[ID];                                             // Delete timer reference
          console.log('timers after:', perfectTimer.timers);
        }
        }       
    }
    
    
    
    
    // Below are some tests
    var timerOne = perfectTimer.set(() => {
        console.log(new Date().toString(), Date.now(), 'timerOne', timerOne);
    }, 1000);
    console.log(timerOne);
    setTimeout(() => {
        perfectTimer.clear(timerOne);
    }, 5000)
    
    var timerTwo = perfectTimer.set(() => {
        console.log(new Date().toString(), Date.now(), 'timerTwo', timerTwo);
    }, 1000);
    console.log(timerTwo);
    
    setTimeout(() => {
        perfectTimer.clear(timerTwo);
    }, 8000)

    0 讨论(0)
提交回复
热议问题