一、背景
在项目中,需要使用倒计时来提醒用户做出相应的操作,并且对倒计时的精准性有比较高的要求。一开始是页面直接获取服务器时间,在页面设置定时器【setInterval(function{…}, 1000);】,但发现各个页面从一开始加载出的时间就不一致,并且用户做出相应的操作时会导致定时器卡住,致使误差更大。
二、原因分析
致使这种情况的大致原因如下:
- 网络通讯需要耗费一定的时间;(暂时没想到办法计算)
- 页面加载需要一定时间,并且不同浏览器的加载时间也不一样;
- JS是单线程运行的,当有其他操作就会致使定时器卡住,等其他操作结束之后再执行,造成误差。
注:此模型中未考虑由于用户的操作引起定时器阻塞导致的时间误差。
二、JS实现较为精准的倒计时
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>倒计时</title>
<script>
var startTime = new Date().getTime(); // 此时时间(用于计算线程占有,以及渲染引起的时间误差)
var i = 0;
while(i < 80000){ // 模拟页面加载(占用线程)
++ i;
}
</script>
<style>
.outerDiv {
width: 880px;
margin: 50px auto;
}
.countDown {
text-align: center;
padding-top: 10%;
padding-bottom: 5%;
line-height: 80px;
font-size: 46px;
color: #333;
}
.countDown i {
color: #fff;
display: inline-block;
border-radius: 10px;
width: 68px;
height: 80px;
background: #ff9f09;
margin: 0 6px;
padding-right: 10px;
}
</style>
</head>
<body>
<div class="outerDiv">
<div id="timeStrDiv" class="countDown">
</div>
</div>
</body>
<script>
window.onload = function(){
countDown();
};
var timer = null;
var showStr = "";
var showDiv = document.getElementById("timeStrDiv");
function countDown(times){
if (times == null || times == '') {
times = 24321; // 从服务器获取得倒计时(单位为毫秒)
times -= (new Date().getTime() - startTime); // 获取倒计时开始时间(减去页面加载时间)
}
if (timer != null) {
clearTimeout(timer);
}
var interval = 1000; // 设定倒计时标准间隔差
startTime = new Date().getTime(); // 此时时间(用于计算线程占有,以及渲染引起的时间误差)
var count = 0; // 标记执行次数
// 判断是否需要倒计时
if (Math.floor(times / interval) > 0) {
timer = setTimeout(countDownStart, interval);
} else {
showStr = "倒计时:<i>00</i>天<i>00</i>小时<i>00</i>分钟<i>00</i>秒";
showDiv.innerHTML = showStr;
}
/** 真正的倒计时 */
function countDownStart(){
count++;
var j = 0;
while(j < (count * 8000000)){ // 模拟逻辑代码(占用线程)
++ j;
}
showStr = formatTime("倒计时:<i>DD</i>天<i>HH</i>小时<i>MM</i>分钟<i>ss</i>秒", times);
showDiv.innerHTML = showStr;
var offset = new Date().getTime() - (startTime + count * interval); // 计算误差
if (offset > interval) { // 若误差超过间隔时间,则立即校正(会出现跳秒的情况)
times -= Math.floor(offset / interval) * interval;
count += Math.floor(offset / interval);
offset = offset % interval;
}
var nextTime = interval - offset; // 标准时间间隔减去此次的误差,为下一次执行的时间
if (Math.floor(times / interval) <= 0) {
clearTimeout(timer);
showStr = "倒计时:<i>00</i>天<i>00</i>小时<i>00</i>分钟<i>00</i>秒";
showDiv.innerHTML = showStr;
} else {
times -= interval;
timer = setTimeout(countDownStart, nextTime);
}
console.log("执行次数:" + count + ", 误差:" + offset + "ms, 下一次执行:" + nextTime + "ms之后, 所剩时间:" + times + "ms");
}
}
/** 格式化时间形式 */
function formatTime(fmt, time){
fmt = fmt.toUpperCase();
var day = formatTimeField(Math.floor(time / (1000 * 60 * 60 * 24)));
var hour = formatTimeField(Math.floor(time / (1000 * 60 * 60)) - (day * 24));
var minute = formatTimeField(Math.floor(time / (1000 * 60)) - (day * 24 * 60) - (hour * 60));
var second = formatTimeField(Math.floor(time / 1000) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60));
var timeField = {
"D+": day,
"H+": hour,
"M+": minute,
"S+": second
};
for(var field in timeField){
if(new RegExp("(" + field + ")").test(fmt)){
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length===1) ? (timeField[field]) : (("00" + timeField[field]).substr(("" + timeField[field]).length )));
}
}
return fmt;
}
/** 格式化时间分量 */
function formatTimeField(number){
if (number <= 9) {
number = "0" + number;
}
return number;
}
</script>
</html>