我需要创建一个简单但准确的计时器。
这是我的代码:
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
对象来获取(毫秒)准确的当前时间。然后将您的逻辑基于当前时间值,而不是计算您的回调执行的频率。
对于一个简单的定时器或时钟,跟踪时间的差异明确:
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
}
我只是建立在Bergi 的回答(特别是第二部分)的基础上,因为我真的很喜欢它的完成方式,但是我希望可以选择在计时器启动后停止计时器(clearInterval()
几乎就像)。Sooo...我已经把它包装成一个构造函数,所以我们可以用它做“客观”的事情。
好的,所以你复制/粘贴...
/**
* 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));
}
}
告诉它该做什么等等...
// 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;
我的意思是,无论如何它对我有用。如果有更好的方法,让我知道。
此处答案中的大多数计时器都会滞后于预期时间,因为它们将“预期”值设置为理想值,并且仅考虑浏览器在此之前引入的延迟。如果您只需要准确的间隔,这很好,但是如果您是相对于其他事件的时间,那么您(几乎)总是会有这种延迟。
要纠正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用这种先发制人的校正添加二次调整,漂移的方差以目标时间为中心。例如,如果您总是有 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的回答准确指出了问题中的计时器不准确的原因。以下是一张上用一个简单的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 毫秒更准确
为了使您的计时器更准确,您必须四舍五入
我同意 Bergi 使用 Date 的观点,但他的解决方案对我来说有点矫枉过正。我只是想让我的动画时钟(数字和模拟 SVG)在第二个更新,而不是超限或欠运行,从而在时钟更新中产生明显的跳跃。这是我放在时钟更新函数中的代码片段:
var milliseconds = now.getMilliseconds();
var newTimeout = 1000 - milliseconds;
this.timeoutVariable = setTimeout((function(thisObj) { return function() { thisObj.update(); } })(this), newTimeout);
它只是计算下一个偶数秒的增量时间,并将超时设置为该增量。这会将我所有的时钟对象同步到第二个。希望这是有帮助的。