如何在 javascript 中创建一个精确的计时器?

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

这是我的暗号:

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

整整3600秒后,它会打印3500秒。

  • 为什么不准确?

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

192260 次浏览

没有比这个更精确的了。

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

至于为什么它不准确,我猜测机器正忙于做其他事情,如您所见,在每次迭代中放慢一点速度是累积起来的。

为什么不准确?

因为您使用的是 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毫秒之后执行回调时,您将看到第二个计数 01235(!).因此,最好更频繁地更新,比如每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
}

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

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

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

我只是建立在 Bergi 的回答(特别是第二部分)一点点,因为我真的很喜欢它做的方式,但我想选择停止计时器一旦启动(像 clearInterval()几乎)。所以... 我把它包装成了一个构造函数,这样我们就可以用它来做对象化的事情了。

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;

我的意思是,这对我很有用,如果有更好的办法,告诉我。

这是一个古老的问题,但我认为我应该分享一些我有时使用的代码:

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

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

要纠正它,你可以跟踪漂移的历史,并用它来预测未来的漂移。通过加入一个二次调整,这个先发制人的修正,方差在漂移中心周围的目标时间。例如,如果你总是得到一个20至40毫秒的漂移,这个调整将转移到 -10至 + 10毫秒左右的目标时间。

Bergi 的回答的基础上,我使用了一个滚动中值来实现我的预测算法。用这种方法只取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));
}

Bergi 的回答准确地指出了为什么问题中的计时器不准确。下面是我对使用 startstopresetgetTime方法的一个简单 JS 定时器的看法:

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

我的一个最简单的实现就在下面。它甚至可以在页面重新加载后依然存在。 :-

密码: 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>

Bergi 的回答的启发,我创建了以下完整的非漂移计时器。我想要的是一种设置计时器的方法,停止它,然后简单地做这件事。

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)

下面是一个解决方案,它在窗口被隐藏时暂停,并且可以通过中止控制器取消。

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

漂移 是 setInterval 的替代品,可以减轻漂移。导入 npm 包,然后像 setInterval/setTimeout 那样使用它:

setDriftlessInterval(() => {
this.counter--;
}, 1000);


setDriftlessInterval(() => {
this.refreshBounds();
}, 20000);

您可以使用一个名为 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/

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

工作演示

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>

演示说明

  • 这是计算出的总时间。
  • 这是我们唯一真正的临时变量。只要有人暂停,我们就把 lastpause设置为 Date.now()。当有人取消暂停,并重新暂停,我们计算的时间差的 Date.now()减去最后一个暂停。

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

现代,完全可编程定时器

这个计时器以 Hertz 为单位获取一个频率,一个最多可以获取四个参数的回调,当前帧索引、当前时间、当前帧理想发生的时间,以及对计时器实例的引用(这样调用者和回调都可以访问它的方法)。

注意: 所有的时间都是基于 performance.now的,并且是相对于页面加载的时刻。

定时器实例有三个 API 方法:

  • 没有反对意见。立即(永久)杀死计时器。 返回下一帧(已取消的帧)的帧索引。
  • 以赫兹为单位,调整计时器的频率,开始 返回以毫秒为单位的隐含间隔。
  • redefine: 接受一个新的回调函数。将其与当前 回调。影响下一帧。返回 undefined

注意: 当通过 setTimeout调用 tick方法时,tick方法显式地传递 this(作为 self)以解决 this引用 window的问题。

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

我放弃了自己的构建,最终使用了这个简洁的 图书馆