我需要创建一个简单但准确的计时器。
这是我的代码:
var seconds = 0;
setInterval(function() {
timer.innerHTML = seconds++;
}, 1000);
在3600秒之后,它打印约3500秒。
为什么不准确?
如何创建准确的计时器?
答案 0 :(得分:91)
为什么不准确?
因为您使用的是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
毫秒后执行回调,您将看到第二个计数{{ 1}},0
,1
,2
,3
(!)。因此,建议更频繁地更新,例如大约每100毫秒,以避免这种跳跃。
然而,有时你真的需要一个稳定的间隔来执行你的回调而不会漂移。这需要一个更有利的策略(和代码),虽然它支付得好(并且注册更少的超时)。这些被称为自我调整计时器。在这里,与预期的时间间隔相比,每个重复超时的确切延迟与实际经过的时间相适应:
5
答案 1 :(得分:10)
我只是在Bergi's answer(特别是第二部分)上构建了一点点,因为我真的很喜欢它的完成方式,但是我想要一旦它启动就停止计时器(如{几乎{1}}。 Sooo ...我把它包装成一个构造函数,所以我们可以做对象'用它做的事情。
好吧,所以你复制/粘贴那个......
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);
我的意思是,无论如何它对我有用。如果有更好的方法,请知道。
答案 2 :(得分:3)
我同意Bergi使用Date,但他的解决方案对我的使用有点过分。我只是希望我的动画时钟(数字和模拟SVG)在第二次更新而不是溢出或运行时在时钟更新中创建明显的跳转。以下是我在时钟更新功能中输入的代码片段:
var milliseconds = now.getMilliseconds();
var newTimeout = 1000 - milliseconds;
this.timeoutVariable = setTimeout((function(thisObj) { return function() { thisObj.update(); } })(this), newTimeout);
它只计算到下一秒的增量时间,并将超时设置为该增量。这将我的所有时钟对象同步到第二个。希望这有用。
答案 3 :(得分:2)
没有比这更准确。
hashCode()
至于为什么它不准确,我猜想机器正在忙于做其他事情,每次迭代加速一点就会增加,如你所见。
答案 4 :(得分:2)
这是一个在窗口隐藏时暂停的解决方案,并且可以使用中止控制器取消。
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();
答案 5 :(得分:1)
这是一个古老的问题,但是我想分享一些我有时使用的代码:
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),可以立即开始运行,并且如果需要查看{{1}的次数,则具有一个计数器。 }已运行。派上用场。
编辑:请注意,这不会进行任何输入检查(例如,delay和repeat是正确的类型。如果您想获取计数或更改,则可能需要添加某种get / set函数。重复值。
答案 6 :(得分:1)
此处答案中的大多数计时器都会滞后于预期时间,因为它们将“ expected”值设置为理想值,并且仅考虑了浏览器在此之前引入的延迟。如果您只需要精确的时间间隔,那很好,但是如果您要相对于其他事件进行计时,那么(几乎)总是会有这种延迟。
要更正它,您可以跟踪漂移历史并使用它来预测未来的漂移。通过使用此先发制人的校正添加辅助调整,漂移的方差集中在目标时间附近。例如,如果您始终会产生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));
}
答案 7 :(得分:0)
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>
该代码段还包括针对您的问题的解决方案。因此,我们无需启动seconds
变量(每隔1000ms间隔),而是启动计时器,然后每100ms *从计时器中读取经过的时间并相应地更新视图。
*-比1000ms更精确
要使计时器更准确,您必须四舍五入
答案 8 :(得分:0)
下面是我最简单的实现之一。它甚至可以在页面重新加载后幸存下来。 :-
密码笔: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>
答案 9 :(得分:0)
这是一个简单计时器的基本实现。 注意:.timer是要显示计时器的类,完成功能是您的实现,它将在计时器完成后触发
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);
}
答案 10 :(得分:0)
受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)
答案 11 :(得分:0)
driftless 是 setInterval 的替代品,可减轻漂移。让生活更轻松,导入 npm 包,然后像 setInterval / setTimeout 一样使用它:
setDriftlessInterval(() => {
this.counter--;
}, 1000);
setDriftlessInterval(() => {
this.refreshBounds();
}, 20000);
答案 12 :(得分:0)
您可以使用名为 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/