代码之家  ›  专栏  ›  技术社区  ›  iveqy

如何在特定时间执行函数

  •  2
  • iveqy  · 技术社区  · 5 年前

    我需要创建一个简单但准确的计时器。

    这是我的代码:

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

    在3600秒之后,它会打印大约3500秒。

    • 为什么不准确?

    • 如何创建准确的计时器?

    0 回复  |  直到 5 年前
        1
  •  200
  •   Bergi    3 年前

    为什么不准确?

    因为你正在使用 setTimeout() setInterval() . They cannot be trusted ,没有精确性保证。 They are allowed to lag 随意的,他们不保持恒定的速度,但是 tend to drift (正如你所观察到的)。

    如何创建准确的计时器?

    使用 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
    }
    
        2
  •  30
  •   Leon Williams    3 年前

    我只是建立在 Bergi's answer (特别是第二部分)有一点,因为我真的很喜欢它的工作方式,但我想在计时器启动后停止它(比如 clearInterval() 差不多)。所以。。。我把它包装成了一个构造函数,这样我们就可以用它做“objecty”的事情了。

    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)
     * @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.实例化

    告诉它该怎么做。。。

    // 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.那就。。。东西

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

    我的意思是,不管怎样,这对我来说很有效。如果有更好的办法,让我知道。

        3
  •  14
  •   Community Neeleshkumar S    4 年前

    Bergi 的答案准确地指出了问题中计时器不准确的原因。下面是我对一个简单的JS定时器的看法 start , stop , reset getTime 方法:

    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>

    这个片段还包含了一个解决问题的方法。所以不是递增 seconds 每1000毫秒一次,我们就启动计时器,然后每100毫秒一次。我们只是从计时器读取经过的时间,并相应地更新视图。

    *-比1000毫秒更精确

    为了让你的计时器更准确,你必须四舍五入

        4
  •  13
  •   Blorf    5 年前

    这里答案中的大多数计时器都会延迟预期时间,因为它们将“预期”值设置为理想值,并且只考虑浏览器在该点之前引入的延迟。如果你只需要精确的时间间隔,这很好,但是如果你是相对于其他事件计时的,那么你(几乎)总是会有这种延迟。

    要纠正它,可以跟踪漂移历史,并使用它预测未来的漂移。通过使用这种先发制人的修正添加二次调整,漂移的变化围绕着目标时间。例如,如果你总是得到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));
    }
        5
  •  7
  •   agent-p    7 年前

    我同意Bergi使用Date,但他的解决方案对我来说有点过头了。我只是想让我的动画时钟(数字和模拟SVG)在第二次更新,而不是在时钟更新中产生明显的跳跃。以下是我在时钟更新函数中输入的代码片段:

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

    它只计算下一个偶数秒的增量时间,并将超时设置为该增量。这会将我所有的时钟对象同步到秒。希望这有帮助。

        6
  •  3
  •   JaffaTheCake    4 年前

    这里有一个解决方案,当窗口被隐藏时会暂停,并且可以通过中止控制器取消。

    function animationInterval(ms, signal, callback) {
      const start = document.timeline.currentTime;
    
      function frame(time) {
        if (signal.aborted) return;
        callback(time);
        scheduleFrame(time);
      }
    
      function scheduleFrame(time) {
        const elapsed = time - start;
        const roundedElapsed = Math.round(elapsed / ms) * ms;
        const targetNext = start + roundedElapsed + ms;
        const delay = targetNext - performance.now();
        setTimeout(() => requestAnimationFrame(frame), delay);
      }
    
      scheduleFrame(start);
    }
    

    用法:

    const controller = new AbortController();
    
    // Create an animation callback every second:
    animationInterval(1000, controller.signal, time => {
      console.log('tick!', time);
    });
    
    // And stop it sometime later:
    controller.abort();
    
        7
  •  1
  •   V. Rubinetti    6 年前

    这是一个老问题,但我想分享一些我有时使用的代码:

    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函数。

        8
  •  1
  •   Daniel Glassford    4 年前

    灵感来自 Bergi's answer 我创建了以下完整的非漂移计时器。我想要的是一种设置计时器的方法,停止它,然后简单地完成。

    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)
        9
  •  1
  •   Dharman chazomaticus    3 年前

    这里的许多答案都很好,但它们的代码示例通常是一页又一页的代码(好的答案甚至有关于复制/粘贴所有代码的最佳方式的说明)。我只是想用一个非常简单的例子来理解这个问题。

    工作演示

    var lastpause = 0;
    var totaltime = 0;
    
    function goFunction(e) {
        if(this.innerText == 'Off - Timer Not Going') {
            this.innerText = 'On - Timer Going';
        } else {
            totaltime += Date.now() - lastpause;
            this.innerText = 'Off - Timer Not Going';
        }
        lastpause = Date.now();
        document.getElementById('count').innerText = totaltime + ' milliseconds.';
    }
    
    document.getElementById('button').addEventListener('click', goFunction);
    <button id="button">Off - Timer Not Going</button> <br>
    Seconds: <span id="count">0 milliseconds.</span>

    演示说明

    • totaltime 这是计算的总时间。
    • lastpause 这是我们唯一真正的临时变量。每当有人点击“暂停”,我们就会设置 最后停顿 Date.now() .当有人取消暂停并再次暂停时,我们计算 日期现在() 从最后的停顿中减去。

    我们只需要这两个变量:我们的总数和上次停止计时器的时间。其他答案似乎使用了这种方法,但我想要一个简洁的解释。

        10
  •  0
  •   php_nub_qq    9 年前

    没有比这更准确的了。

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

    至于为什么不准确,我猜机器正在忙于做其他事情,每次迭代加起来都会稍微慢一点,如你所见。

        11
  •  0
  •   Shiva    4 年前

    下面是我最简单的实现之一。它甚至可以在页面重新加载后继续使用-

    代码笔: 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>
        12
  •  0
  •   Grant    3 年前

    driftless 是降低漂移的setInterval替代品。让生活变得简单,导入npm包,然后像setInterval/setTimeout那样使用它:

    setDriftlessInterval(() => {
        this.counter--;
    }, 1000);
    
    setDriftlessInterval(() => {
        this.refreshBounds();
    }, 20000);
    
        13
  •  0
  •   agc    3 年前

    您可以使用一个名为setTimeout的函数来设置倒计时。

    首先,创建一个javascript代码片段并将其添加到页面中,如下所示:;

    var remainingTime = 30;
        var elem = document.getElementById('countdown_div');
        var timer = setInterval(countdown, 1000); //set the countdown to every second
        function countdown() {
          if (remainingTime == -1) {
            clearTimeout(timer);
            doSomething();
          } else {
            elem.innerHTML = remainingTime + ' left';
            remainingTime--; //we subtract the second each iteration
          }
        }
    

    来源+更多详细信息-> https://www.growthsnippets.com/30-second-countdown-timer-javascript/

        14
  •  0
  •   CYPS84    3 年前

    现代的、完全可编程的定时器

    该计时器采用赫兹频率,以及一个最多可以使用四个参数的回调、当前帧索引、当前时间、当前帧理想出现的时间,以及对计时器实例的引用(因此调用方和回调方都可以访问其方法)。

    注:所有时间均基于 performance.now ,并与页面加载的时刻相关。

    计时器实例有三种API方法:

    • stop :不需要args。立即(永久)终止计时器。 返回下一帧(取消的帧)的帧索引。
    • adapt :获取以赫兹为单位的频率,并根据该频率调整计时器,开始 从下一帧开始。返回以毫秒为单位的隐含间隔。
    • redefine :接受一个新的回调函数。用电流交换 回拨。影响下一帧。退换商品 undefined .

    注:该 tick 方法通过 this 明确地围绕 self )来解决 引用 window 打上钩 方法通过 setTimeout .

    class ProgrammableTimer {
    
        constructor(hertz, callback) {
    
            this.target = performance.now();     // target time for the next frame
            this.interval = 1 / hertz * 1000;    // the milliseconds between ticks
            this.callback = callback;
            this.stopped = false;
            this.frame = 0;
    
            this.tick(this);
        }
    
        tick(self) {
    
            if (self.stopped) return;
    
            const currentTime = performance.now();
            const currentTarget = self.target;
            const currentInterval = (self.target += self.interval) - currentTime;
    
            setTimeout(self.tick, currentInterval, self);
            self.callback(self.frame++, currentTime, currentTarget, self);
        }
    
        stop() { this.stopped = true; return this.frame }
    
        adapt(hertz) { return this.interval = 1 / hertz * 1000 }
    
        redefine(replacement) { this.callback = replacement }
    }