1、背景
前端页面倒计时功能在很多场景中会用到,如手机端的欢迎页倒计时、商城功能的秒杀活动等,这些功能往往对时间的精确更高,下面分享下常见的坑点及如何解决。
2、一般实现存在的问题
代码如下:
var second = 10, interval = 1000; // 倒计时时间为10s
var oTime = $("#countdown");
var timer = null, start = new Date().getTime(), count = 0;
$("#countdown").text(`${second}s`);
timer = setInterval(handleTime, interval);
function handleTime () {
if (second === 0) {
// ... // 省略
clearInterval(timer);
return false;
}
count++;
console.log(new Date().getTime() - (start + count * interval)); // 定时器每秒执行一次,每次输出 应该 为0
second--;
$("#countdown").text(`${second}s`);
}
以上代码实际输出如下:
结论:由于代码执行占用时间和其他事件阻塞原因,导致有些事件执行延迟了几ms,但影响还不是很大。
下面加一段阻塞线程的代码:
var start = new Date().getTime();
var count = 0, interval = 1000;
// 占用线程事件
setInterval(function () {
var n = 0;
while(n++ < 1000000000);
}, 0);
// 定时器测试
setInterval(function () {
count++;
console.log(new Date().getTime() - (start + count * interval));
}, interval);
以上代码实际输出如下:
结论:由于加了很占线程的阻塞事件,导致定时器事件每次执行延迟越来越严重。
以上的阻塞线程的代码还不算很极端,假如在执行定时器的过程中有同步 ui 事件的代码,同步代码会立即执行。实际上在移动端的滚动页面中是有可能出现这种情况的,以下是一个例子:
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 秒 |----1s----|----2s----|----3s----|--->console.log("Done!");
经过 2 秒 |----1s----|----2s----|-----------|--->console.log("Done!");
点击 body 后:
以为是这样:|----1s----|----2s----|----3s----|--->console.log("Done!")--->|------------------10s----------------|
其实是这样:|----1s----|----2s----|-------------------10s-------------------|--->console.log("Done!");
结论:如果有同步的 ui 事件代码出现,实际功能的倒计时基本“失效”了,这是不同浏览器打开相同的倒计时页面往往误差非常大。
3、解决思路
分析一下从获取服务器时间到前端显示倒计时的过程:
- 客户端 http 请求服务器时间;
- 服务器响应完成;
- 服务器通过网络传输时间数据到客户端;
- 客户端根据活动开始时间和服务器时间差做倒计时显示;
服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十 ms 以内,大的可能有几百 ms 。
可以得出:当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选),这里重点是说要考虑前端渲染的时间,避免不同浏览器渲染快慢差异造成明显的时间不同步,这是第一点。(网络传输时间忽略或加个常量),前端渲染时间可以在服务器返回当前时间和本地前端的时间的差值得出。
获得服务器时间后,前端进入倒计时计算和计时器显示,这步就要考虑 js 代码冻结和线程阻塞造成计时器延时问题了,思路是通过引入计数器,判断计时器延迟执行的时间来调整,尽量让误差缩小,不同浏览器不同时间段打开页面倒计时误差可控制在 1s 以内。
// 继续线程占用
setInterval(function () {
var n = 0;
while(n++ < 1000000000);
}, 0);
// 倒计时
var interval = 1000,
ms = 50000, // 从服务器和活动开始时间计算出的时间差,这里测试用50000ms
timer = null,
start = new Date().getTime(),
count = 0;
if (ms >= 0) {
timer = setTimeout(countDownStart, interval);
}
function countDownStart() {
var offset, nextTime;
count++;
offset = new Date().getTime() - (start + count * interval);
nextTime = interval - offset;
if (nextTime < 0) { nextTime = 0; }
ms -= interval;
console.log("误差: " + offset + "ms, 下一次执行: " + nextTime + "ms后,离活动开始还有: " + ms + "ms");
if (ms < 0) {
clearTimeout(timer);
} else {
timer = setTimeout(countDownStart, nextTime);
}
}
运行结果如下:
结论:由于线程阻塞延迟问题,做了 setTimeout 执行时间的误差修正,保证 setTimeout 执行时间一致。若冻结时间特别长的,还要做特殊处理。
4、京东首页秒杀的实现
从以上代码截图可以看出,京东秒杀的实现实际上和本文的所述实现是一致的,通过计算误差来执行定时器。
总结:简单来说,用 setTimeout 来实现,不能用 setInterval。