如何在javascript中创建一个准确的计时器?
Posted
技术标签:
【中文标题】如何在javascript中创建一个准确的计时器?【英文标题】:How to create an accurate timer in javascript? 【发布时间】:2015-07-10 09:38:08 【问题描述】:我需要创建一个简单但准确的计时器。
这是我的代码:
var seconds = 0;
setInterval(function()
timer.innerhtml = seconds++;
, 1000);
恰好在 3600 秒之后,它会打印大约 3500 秒。
为什么不准确?
如何创建准确的计时器?
【问题讨论】:
更改 timer.innerHTML 并不总是即时。您是如何衡量“正确”时间的? 您受制于浏览器(和主机操作系统)。您可以使用setTimeout()
而不是setInterval()
,跟踪时间漂移并相应地调整每个睡眠时间。
您可能会尝试使用 setTimeout 而不是 setInterval 使用各种回调。也许另一种方法是使用时间对象?也许渲染时刻和当前时间之间的差异更精确(但不确定)。
如果不能使用Date
对象,也可以依赖setTimeout()
函数,每隔N秒通过ajax与服务器更新的time()
重新同步.
有趣的是,它的时间更少,而不是更多。我可以完全理解它需要更长的时间,但更少是很奇怪的。
【参考方案1】:
为什么不准确?
因为您使用的是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
【讨论】:
对第二个计时器代码做一点解释会很有帮助 这在很大程度上取决于您的计时器在做什么。大多数时候,有比一次执行多次step
更好的方法。如果您使用差异策略(而不是计数器) - 无论计时器是否自动调整,您都应该这样做 - 您只需要一个步骤,它就会自动赶上。
@nem035 哦,好吧,我还没有回答足够多的问题 :-)
@JoshuaMichaelCalafell 取决于您的用例,当步骤严重延迟时该怎么办。如果您什么都不做,它会尝试通过尽快触发尽可能多的步骤来赶上,这实际上可能对动画等有害。您可以执行expected += dt
(与expected = Date.now()
相同)或expected += interval * Math.floor(dt / interval)
,这将只需跳过错过的步骤 - 可能适用于 delta 的所有内容。如果您以某种方式计算步数,那么您可能需要对其进行调整。
上面写的计时器几乎总是比预期的时间晚一点点。如果您只需要准确的间隔,这很好,但如果您是相对于其他事件进行计时,那么您将始终有这个小延迟。为了纠正这个问题,您还可以跟踪漂移历史并添加一个辅助调整,使漂移围绕目标时间居中。例如,如果您得到 0 到 30 毫秒的漂移,这会将其调整为 -15 到 +15 毫秒。我使用滚动中位数来计算随时间变化的差异。仅抽取 10 个样本就可以产生合理的差异。【参考方案2】:
我只是在Bergi's answer(特别是第二部分)的基础上做了一点点,因为我真的很喜欢它的完成方式,但我希望选择在计时器启动后停止计时器(就像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;
我的意思是,无论如何它都对我有用。如果有更好的方法,请告诉我。
【讨论】:
如果您愿意使用 RxJS,您可以使用 Observable.timer() 和各种运算符来实现类似的解决方案:stackblitz.com/edit/… 我知道这篇文章有点老了,但好奇是否有人知道如何调整它,以便元素可以拥有自己的计时器。因此,当您滚动页面时,您可以让各种元素计算它们的时间。这是我试图解决的问题,如下所示:***.com/questions/70019023/… 当用户长时间离开浏览器选项卡/窗口(例如chrome)时,此方法是否也有效? (据我观察,超过 10-20 分钟足以“阻止”计时器) @ andreif no,您不能依赖于选项卡处于非活动状态时超时。您需要做的是在开始时创建一个时间戳,然后让每个间隔/超时创建一个新的时间戳并减去开始时间戳,以查看自开始以来实际经过了多少时间。因此,例如,如果您正在制作秒表,则不会使用超时的延迟来衡量时间的流逝。您将使用时间戳进行测量,间隔延迟就是您根据当前持续时间更新事物(例如手部移动)的频率。【参考方案3】:Bergi 的回答准确指出了问题中的计时器不准确的原因。这是我对带有start
、stop
、reset
和getTime
方法的简单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>
sn-p 还包括针对您的问题的解决方案。因此,我们不是每 1000 毫秒间隔递增 seconds
变量,而是启动计时器,然后每 100 毫秒* 我们只是从计时器读取经过的时间并相应地更新视图。
* - 使其比 1000 毫秒更准确
为了让你的计时器更准确,你必须四舍五入
【讨论】:
【参考方案4】:此处答案中的大多数计时器都会在预期时间之后徘徊,因为它们将“预期”值设置为理想值,并且仅考虑浏览器在此之前引入的延迟。如果您只需要准确的间隔,这很好,但如果您是相对于其他事件进行计时,那么您将(几乎)总是有这种延迟。
要纠正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用这种先发制人的校正添加二次调整,漂移的方差以目标时间为中心。例如,如果您总是得到 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));
【讨论】:
如果所有内容(在脚本中)都是单线程的,那么数组如何变异? @CYPS84 每次运行 step 函数时都会更新数组,每次运行时由 setTimeout 重新调度。调度允许在每次运行之间发生其他事情。【参考方案5】:我同意 Bergi 对使用 Date 的看法,但他的解决方案对我来说有点矫枉过正。我只是想让我的动画时钟(数字和模拟 SVG)在第二个更新,而不是超限或运行不足,从而在时钟更新中产生明显的跳跃。这是我在时钟更新函数中输入的代码 sn-p:
var milliseconds = now.getMilliseconds();
var newTimeout = 1000 - milliseconds;
this.timeoutVariable = setTimeout((function(thisObj) return function() thisObj.update(); )(this), newTimeout);
它只是计算下一个偶数秒的增量时间,并将超时设置为该增量。这会将我所有的时钟对象同步到秒。希望这会有所帮助。
【讨论】:
【参考方案6】:这是一个在窗口隐藏时暂停的解决方案,并且可以使用中止控制器取消。
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();
【讨论】:
21 世纪的解决方案! Gist 有详细的视频解释。谢谢杰克!【参考方案7】:这是一个老问题,但我想我会分享一些我有时使用的代码:
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 函数重复值。
【讨论】:
我很好奇。为什么(this.count+(this.runAtStart ? 2 : 1)
使用runAtStart = true 似乎会使第一步花费太长时间,如果不是runAtStart = true,似乎也会使第一帧太长。 ?
我想我真正想问的是,有什么理由不能就这样吗? var adjustedDelay = Math.max( 1, this.startTime + (this.count * this.delay ) - performance.now() );
老实说我不记得了。这段代码在这一点上已经很老了,可能会被重写得更干净。只是想复制/粘贴共享以显示与其他答案不同的方法,使用指定的run N times
参数,我发现它非常有用(如果需要,您也可以使用原型) .【参考方案8】:
受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】:这里的许多答案都很棒,但它们的代码示例通常是一页又一页的代码(好的答案甚至有关于复制/粘贴所有内容的最佳方式的说明)。我只是想通过一个非常简单的例子来理解这个问题。
工作演示
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>
Demo讲解
totaltime
— 这是计算的总时间。
lastpause
— 这是我们拥有的唯一真正的临时变量。每当有人点击暂停时,我们将lastpause
设置为Date.now()
。当有人取消暂停并再次暂停时,我们会计算从上次暂停中减去 Date.now()
的时间差异。
我们只需要这两个变量:我们的总数和我们上次停止计时器的时间。其他答案似乎使用这种方法,但我想要一个简洁的解释。
【讨论】:
【参考方案10】:现代、完全可编程的定时器
此计时器采用以赫兹为单位的频率,以及一个最多可采用四个参数的回调,即当前帧索引、当前时间、当前帧理想发生的时间以及对计时器实例的引用(所以调用者和回调都可以访问它的方法)。
注意:所有时间均基于performance.now
,并且与页面加载的时刻相关。
Timer 实例具有三种 API 方法:
stop
:不接受任何参数。立即(并永久)终止计时器。
返回下一帧(取消的帧)的帧索引。
adapt
: 以赫兹为单位获取频率并调整计时器以适应它,开始
从下一帧开始。返回以毫秒为单位的隐含间隔。
redefine
:采用新的回调函数。将其与当前交换
打回来。影响下一帧。返回undefined
。
注意:tick
方法通过显式传递this
(如self
)来解决this
在通过tick
方法被setTimeout
调用时引用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
【讨论】:
我不确定 performance.now 是什么,但它似乎是这里工作的正确工具“与 Date.now() 不同,performance.now() 返回的值总是增加以恒定速率,独立于系统时钟(可能手动调整或由 NTP 等软件倾斜)。否则,performance.timing.navigationStart + performance.now() 将大约等于 Date.now()。" 是的,performance.now
实际上只是Date.now
等等的更准确的版本。我刚刚发布了一个更完整的计时器实现,其中包含您最常需要的功能。【参考方案11】:
没有比这更准确的了。
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);
至于为什么它不准确,我猜机器正忙于做其他事情,每次迭代都会减慢一点,正如你所见。
【讨论】:
当用户开始更改他们的操作系统时间时,事情变得很有趣。 @eithedog 没有比这更好的了。确实有,等一下! 除非您希望暂停 2 秒,然后显示的数字从 10 跳到 12,否则请更频繁地进行此更新(例如 100 毫秒而不是 1000 毫秒) @James 好吧,如果你的机器死机 2 秒,我看不出更频繁的更新会有什么帮助。 我会删除警报,因为如果我使用计时器,只是为了好奇,我会继续并更改当前系统时间,因为这就像你在间接告诉我“咦,如果你这样做会怎样?!?!?!” imgs.xkcd.com/comics/the_difference.png【参考方案12】:下面是我最简单的实现之一。它甚至可以在页面重新加载后存活下来。 :-
密码笔: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>
【讨论】:
【参考方案13】:driftless 是 setInterval 的替代品,可减轻漂移。让生活变得简单,导入 npm 包,然后像 setInterval / setTimeout 一样使用它:
setDriftlessInterval(() =>
this.counter--;
, 1000);
setDriftlessInterval(() =>
this.refreshBounds();
, 20000);
【讨论】:
感谢分享。不幸的是,在我的用例中,时间仍然无处不在。我的问题是整秒——不是毫秒。我得找点别的了 @velkoon 我注意到这个解决方案在离开 Chrome 上的标签时有问题 - 但是当你快速导航回来时它会自动恢复。我认为requestAnimationFrame
解决方案可能就是其中之一。如果您找到万无一失的解决方案,请告诉我。【参考方案14】:
您可以使用一个名为 setTimeout 的函数来设置倒计时。
首先,创建一个javascript sn-p并将其添加到您的页面中,如下所示;
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/
【讨论】:
以上是关于如何在javascript中创建一个准确的计时器?的主要内容,如果未能解决你的问题,请参考以下文章
如何减少在 Swift spritekit 中创建更多敌人的时间?
我如何在 cocos2d 中创建 30 分钟倒计时计时器,就像糖果迷生活一样。
如何在 C# 中创建计时器而不使用 System.Timers.Timer 或 System.Threading.Timer