背景
前端页面倒计时功能在很多场景中会用到,如广告页面的倒计时,活动倒计时,最常见的是手机获取验证码后,1min/30s的倒计时。
传统的实现方式
example 1
var timer;
var timer_div = $('#timer_div');
var second = 10; // 倒计时时间为 10 s
var start = new Date().getTime(); //获取当前时间
var count = 0;
clearInterval(timer);
timer = setInterval(showTime, 1000);
function showTime() {
if (second === 0) {
... // 业务代码
clearInterval(timer);
return false;
}
count++;
console.log(new Date().getTime() - (start + count * 1000)); // 按我们正常的预期,是想着定时器每秒执行一次,每次输出应该是0 。
timer_div.html('<div>' + second + 's</div>');
second--;
}
结果
可以很明显看到,这里输出的并不是我们预期的结果。接下来我们来看来一个较极端的例子
example2
function runForSeconds(s) {
var start = +new Date();
while (start + s * 1000 > (+new Date())) {}
}
document.body.addEventListener("click", function () {
runForSeconds(10);
}, false);
setTimeout(function () {
console.log("Done!");
}, 1000 * 3);
结果
3秒内点击 body 后
以为是这样:
|----1s----|----2s----|----3s----|--->console.log("Done!")--->|----10s-----|
其实是这样:
|----1s----|----2s----|--------10s--------|--->console.log("Done!");
结论
由于代码执行占用时间或是假如在执行定时器的过程中有同步UI事件的代码,同步代码会立即执行,该优先执行的代码快的耗时将导致倒计时往往误差非常大。(实际上在移动端的滚动页面中是有可能出现这种情况的)
优化后的方案
// 线程占用
setInterval(function () {
var j = 0;
while(j++ < 100000000);
}, 0);
//倒计时
var interval = 1000,
ms = 50000, // 倒计时长 50000ms
count = 0,
startTime = new Date().getTime();
if (ms >= 0) {
var timeCounter = setTimeout(countDownStart, interval);
}
function countDownStart() {
count++;
var offset = new Date().getTime() - (startTime + count * interval);
var nextTime = interval - offset;
var daytohour = 0;
if (nextTime < 0) { nextTime = 0 };
ms -= interval;
console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
if (ms < 0) {
clearTimeout(timeCounter);
} else {
timeCounter = setTimeout(countDownStart, nextTime);
}
}
结果
此方案的不同点在于,对线程阻塞的延迟问题,做了setTimeout执行时间的误差修正,保证 setTimeout 执行时间一致。若冻结时间特别长的,还要做特殊处理。
tip:以上是在学习了前辈的分享后做的一点小记录,有兴趣的可以交流交流。在此感谢wjq前辈