我需要创建一个简单但准确的计时器。
这是我的代码:
var seconds = 0;
setInterval(function() {
timer.innerHTML = seconds++;
}, 1000);
恰好在3600秒后,它将打印约3500秒。
为什么不准确?
如何创建准确的计时器?
我需要创建一个简单但准确的计时器。
这是我的代码:
var seconds = 0;
setInterval(function() {
timer.innerHTML = seconds++;
}, 1000);
恰好在3600秒后,它将打印约3500秒。
为什么不准确?
如何创建准确的计时器?
setTimeout()
代替setInterval()
,跟踪时间漂移并相应地调整每个睡眠时间。
Date
对象,则还可以依赖该setTimeout()
功能,并time()
每隔N秒通过ajax与服务器的更新进行重新同步。
Answers:
为什么不准确?
因为您正在使用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
现在,这可能会出现值跳跃的问题。当间隔滞后一点,并执行后回调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
一次执行多次更好的方法。如果您使用差异策略(而不是计数器),则无论计时器是否是自调节的,都应使用该策略,您只需执行一个步骤,它就会自动追赶。
expected += dt
(同expected = Date.now()
),或者expected += interval * Math.floor(dt / interval)
这将只是跳过错过的步骤-可能罚款一切与三角洲合作。如果您正在计算步骤数,则可能需要进行调整。
我只是基于Bergi的答案(特别是第二部分),因为我真的很喜欢它的完成方式,但是我希望该选项一旦启动就停止计时器(clearInterval()
几乎一样)。如此...我已经将其包装到构造函数中,以便我们可以用它进行“客观的”事情。
好了,所以您复制/粘贴...
/**
* 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));
}
}
告诉它怎么做以及所有这些...
// 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);
// 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;
我的意思是,无论如何它对我有用。如果有更好的方法,莱姆知道。
答案中的大多数计时器都会滞后于预期时间,因为它们将“ expected”值设置为理想值,并且仅考虑了浏览器在此之前引入的延迟。如果您只需要精确的时间间隔,这很好,但是如果您要相对于其他事件进行计时,那么(几乎)总是会有这种延迟。
要更正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用这种先发制人的校正添加辅助调整,漂移的方差集中在目标时间附近。例如,如果您总是得到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使用Date的观点,但是他的解决方案对于我的使用来说有点过大了。我只是想让我的动画时钟(数字和模拟SVG)在第二秒更新,而不是超速或欠速运行,从而在时钟更新中产生明显的跳跃。这是我在时钟更新函数中放入的代码片段:
var milliseconds = now.getMilliseconds();
var newTimeout = 1000 - milliseconds;
this.timeoutVariable = setTimeout((function(thisObj) { return function() { thisObj.update(); } })(this), newTimeout);
它仅计算到下一秒的增量时间,并将超时设置为该增量。这会将我所有的时钟对象同步到第二个。希望这会有所帮助。
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
,我们不用启动变量,而是每1000ms间隔增加变量,而是每100ms *从定时器中读取经过的时间并相应地更新视图。
*-比1000ms更准确
为了使计时器更准确,您必须四舍五入
没有比这更准确的了。
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);
至于为什么它不准确,我猜想这台机器正忙于做其他事情,如您所见,每次迭代的速度都会有所降低。
这是一个古老的问题,但是我想分享一些我有时使用的代码:
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()
运行了多少次,还可以拥有一个计数器。派上用场。
编辑:注意,这不会做任何输入检查(例如,如果delay和repeat是正确的类型。并且,如果您想获取计数或更改重复值,则可能要添加某种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
参数,我发现它非常有用(而且如果需要,您可以使用原型进行此操作)。
我最简单的实现之一在下面。它甚至可以在页面重新加载后幸存下来。:-
代码笔: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>
这是简单计时器的基本实现。注意:.timer是要显示计时器的类,done函数是您的实现,它将在计时器完成后触发
function TIMER() {
var fiveMinutes = 60 * 5,
display = document.querySelector('.timer');
startTimer(fiveMinutes, display);
}
var myInterval;
function startTimer(duration, display) {
var timer = duration, minutes, seconds;
myInterval = setInterval(function () {
minutes = parseInt(timer / 60, 10)
seconds = parseInt(timer % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
display.textContent = minutes + ":" + seconds;
if (--timer < 0) {
clearInterval(myInterval);
doneFunction();
}
}, 1000);
}
受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)