前端倒计时Countdown Timer
前端倒计时偏移的原因
前端网页倒计时是非常常见的应用,我们在各大购物网站的秒杀活动中总是能见到它的身影。但是在实际情况中,我们常常会发现当网页不刷新、让倒计时程序持续运行时,显示时间相比实际时间会越来越慢,相信大家也有在秒杀时间即将到来时不停刷新页面的经历。原因自然也不难理解:倒计时通常使用定时器(setTimeout 或者 setInterval )实现,而 JavaScript 的单线程特性使得主线程执行栈中出现阻塞时,任务队列中的异步任务并不能及时执行,因此浏览器并不能保证在定时器设置的时间结束后代码总是被准时执行,这就造成了倒计时的偏差。
使用setInterval的坑
在使用 setInterval
时,有一些常见的坑需要注意:
- 间隔时间不准确:
setInterval
的执行时间并不是精确的,它会受到 JavaScript 主线程的执行情况和其他任务的影响。如果前一个任务执行时间较长或浏览器性能较差,可能会导致setInterval
的回调函数执行间隔变长,进而影响倒计时的准确性。 - 任务积压:如果回调函数的执行时间超过了设定的间隔时间,那么下一个回调函数将会等待前一个任务执行完毕后立即执行,而不是按照设定的间隔时间进行。这可能导致任务积压,回调函数堆积在任务队列中,最终导致性能下降。
- 内存泄漏:如果
setInterval
没有被正确清除,它会持续触发回调函数,即使你不再需要它。这会导致内存泄漏,因为定时器的引用仍然存在,而回调函数不会被垃圾回收。
为了避免这些问题,可以采取以下措施:
- 使用
setTimeout
替代setInterval
:通过在回调函数执行完毕后再次设置setTimeout
,可以精确控制回调函数的执行间隔,避免任务积压和准确性问题。 - 明确清除定时器:在不需要继续执行定时器时,使用
clearTimeout
或clearInterval
清除定时器,避免内存泄漏。 - 考虑使用更精确的定时器方案:如使用
requestAnimationFrame
或Web Workers
来实现更精确的定时器,尤其是在需要高精度的时间控制时。 - 注意任务的执行时间:确保回调函数的执行时间尽可能短小,并及时处理可能导致长时间执行的情况,以确保定时器的准确性。
使用 setTimeout 实现 setInterval?怎么模拟?
在JavaScript中,使用setTimeout
函数可以实现周期性重复执行的效果,就像setInterval
函数一样。setInterval
函数会按照指定的时间间隔无限次地重复执行一个函数,而setTimeout
函数只会执行一次,但可以通过在函数内部再次调用setTimeout
来实现周期性重复执行的效果。
使用setTimeout
来模拟setInterval
的原因可能有几个:
- 灵活性:使用
setTimeout
可以更灵活地控制每次执行之间的时间间隔。通过在每次函数执行完后再次调用setTimeout
,可以根据需要来调整下一次执行的时间间隔,而不是固定一个时间间隔。 - 避免积累:
setInterval
函数的执行间隔是固定的,如果某次执行的时间超过了指定的间隔,下一次执行将会被推迟,这可能导致多次执行积累在一起。而使用setTimeout
,可以确保每次执行都在前一次执行完成后的指定时间间隔之后进行。
下面是一个使用setTimeout
来模拟setInterval
的简单示例:
function simulateSetInterval(func, interval) {
function intervalFunction() {
func();
setTimeout(intervalFunction, interval);
}
setTimeout(intervalFunction, interval);
}
// 使用示例
function sayHello() {
console.log("Hello!");
}
// 每隔1秒钟输出一次 "Hello!"
simulateSetInterval(sayHello, 1000);
在上述示例中,simulateSetInterval
函数接受一个需要周期性执行的函数func
和一个时间间隔interval
作为参数。它定义了一个intervalFunction
函数,该函数在每次执行完func
后,通过setTimeout
再次调用自身以实现周期性的重复执行。最后,通过调用setTimeout(intervalFunction, interval)
来启动第一次执行。
需要注意的是,使用setTimeout
模拟的setInterval
在某些情况下可能会存在一定的误差,因为setTimeout
依赖于系统时间。如果函数执行的时间加上下一个setTimeout
的时间间隔超过了指定的间隔,那么下一次执行将会被推迟。这种情况下,使用setInterval
可能更加准确。
前端根据偏差时间来自动调整间隔时间
setTimeout
递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,每次都将变量+1,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间
将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来纠正由于程序执行所造成的时间误差.
function startCountdown(targetTime) {
var interval = 1000; // 每秒执行一次
var offset = 0; // 时间偏差
var startTime = new Date().getTime(); // 开始倒计时的时间戳
function updateCountdown() {
var currentTime = new Date().getTime(); // 当前时间戳
var elapsed = currentTime - startTime - offset; // 累计经过的时间(考虑偏差)
var remainingTime = targetTime - elapsed; // 剩余时间
if (remainingTime <= 0) {
// 倒计时结束
console.log('倒计时结束');
return;
}
// 处理倒计时逻辑,如显示剩余时间等
console.log('剩余时间:', remainingTime);
// 计算下一次执行的时间间隔
var nextTime = interval - (currentTime - startTime) % interval;
// 纠正偏差
offset = currentTime - startTime - elapsed;
// 设置下一次定时器
setTimeout(updateCountdown, nextTime);
}
// 启动倒计时
updateCountdown();
}
// 示例:倒计时目标时间为 10分钟后
var targetTime = new Date().getTime() + 10 * 60 * 1000;
startCountdown(targetTime);
前端定时向服务器发送请求获取最新的时间差
// 获取服务器时间
function getServerTime() {
return new Promise(function(resolve, reject) {
// 发送AJAX请求获取服务器时间,假设返回的数据格式为 { timestamp: 1234567890 }
// 这里使用setTimeout模拟异步请求
setTimeout(function() {
var serverTime = { timestamp: 1234567890 };
resolve(serverTime);
}, 1000);
});
}
// 获取客户端时间
function getClientTime() {
return Date.now();
}
// 计算时间偏差
function calculateTimeOffset(serverTime, clientTime) {
return serverTime - clientTime;
}
// 倒计时纠偏实现
function countdownWithOffset(targetTime) {
var serverTimePromise = getServerTime();
var clientTime = getClientTime();
serverTimePromise.then(function(serverTime) {
var timeOffset = calculateTimeOffset(serverTime.timestamp, clientTime);
var adjustedTargetTime = targetTime + timeOffset;
setInterval(function() {
var currentTime = getClientTime();
var remainingTime = adjustedTargetTime - currentTime;
// 处理倒计时逻辑,如显示剩余时间等
console.log('Remaining Time:', remainingTime);
}, 1000);
});
}
// 示例:倒计时目标时间为 10分钟后
var targetTime = Date.now() + 10 * 60 * 1000;
countdownWithOffset(targetTime);
在这个示例中,getServerTime
函数模拟通过发送AJAX请求获取服务器时间,返回一个Promise对象。getClientTime
函数使用Date.now()
获取客户端的当前时间戳。
calculateTimeOffset
函数计算时间偏差,即服务器时间减去客户端时间。
countdownWithOffset
函数是倒计时纠偏的实现。它获取服务器时间,并计算时间偏差。然后,使用调整后的目标时间进行倒计时。
在setInterval
中,根据客户端时间和时间偏差计算剩余时间,并处理倒计时逻辑。这里仅打印剩余时间作为示例。
最后,通过调用countdownWithOffset
函数并传入倒计时的目标时间来启动倒计时纠偏。
两种方法结合
function Countdown(targetTime) {
var interval = 1000; // 每秒执行一次
var offset = 0; // 时间偏差
var startTime = new Date().getTime(); // 开始倒计时的时间戳
var serverTime = null; // 服务器时间
// 获取服务器时间
function getServerTime() {
return new Promise(function(resolve, reject) {
// 发送AJAX请求获取服务器时间,假设返回的数据格式为 { timestamp: 1234567890 }
// 这里使用setTimeout模拟异步请求
setTimeout(function() {
var serverTime = { timestamp: 1234567890 };
resolve(serverTime);
}, 1000);
});
}
// 获取客户端时间
function getClientTime() {
return Date.now();
}
// 计算时间偏差
function calculateTimeOffset() {
return serverTime.timestamp - getClientTime();
}
// 启动倒计时
this.start = function() {
getServerTime().then(function(time) {
serverTime = time;
offset = calculateTimeOffset();
startTime = getClientTime();
updateCountdown();
});
};
// 更新倒计时
function updateCountdown() {
var currentTime = getClientTime();
var elapsed = currentTime - startTime - offset; // 累计经过的时间(考虑偏差)
var remainingTime = targetTime - elapsed; // 剩余时间
if (remainingTime <= 0) {
// 倒计时结束
console.log('倒计时结束');
return;
}
// 处理倒计时逻辑,如显示剩余时间等
console.log('剩余时间:', remainingTime);
// 计算下一次执行的时间间隔
var nextTime = interval - (currentTime - startTime) % interval;
// 纠正偏差
offset = calculateTimeOffset();
// 设置下一次定时器
setTimeout(updateCountdown, nextTime);
}
}
// 示例:倒计时目标时间为 10分钟后
var targetTime = Date.now() + 10 * 60 * 1000;
var countdown = new Countdown(targetTime);
countdown.start();
转载自:https://juejin.cn/post/7241374490491191352