倒计时的精确度
先放代码,需要自定义 setTimeout
查看代码
/**
*
* @param {number} timeStemp 倒计时时间撮
* @param {number} delay 延时秒数
* @param {number} init 是否自执行 默认 true
* @param {Function} success 延时执行函数
* @param {Function} complete 轮询执行完成后回调函数
*
*/
function CountdownUtil(options) {
var _options = Object.assign({
timeStemp,
delay: 1000,
init: true,
success: null,
complete: null
}, options);
var timeStemp = _options.timeStemp;
var delay = _options.delay;
var timer;
var count = 0;
var stop = true;
var startTime;
// 开始
this.start = function() {
stop = false;
startTime = new Date().getTime();
startCountDown(delay);
}
// 暂停
this.stop = function() {
if (!timer) {
throw Error('当前定时器不存在');
}
stop = true;
}
// 清除倒计时
this.clear = function() {
stop = true;
timer = null;
}
if (_options.init) {
this.start();
}
var _this = this;
function startCountDown(interval) {
if (stop) {
return;
}
timer = setTimeout(function() {
clearTimeout(timer);
// 倒计时还有多久结束
var restTime = timeStemp - delay * count;
var fomatTime = restTime;
if (restTime < 0) {
restTime = 0;
}
fomatTime = fomatTimeStemp(fomatTime);
// 执行轮询回调
_options.success && _options.success(Object.assign(fomatTime, { diff: restTime }));
// 倒计时结束
if (!restTime) {
_this.clear();
_options.complete && _options.complete();
return;
}
count++;
var endTime = new Date().getTime();
// 当前差值 = 轮询的当前时间 - (最初时间 + 间隔时间 * 轮询次数)
var diff = endTime - (startTime + delay * count);
if (diff < 0) {
diff = 0;
}
console.log(`diff: ${diff}`);
startCountDown(delay - diff);
}, interval);
}
function fomatTimeStemp(total) {
const seconds = Math.floor( (total/1000) % 60 );
const minutes = Math.floor( (total/1000/60) % 60 );
const hours = Math.floor( (total/(1000*60*60)) % 24 );
const days = Math.floor( total/(1000*60*60*24) );
return {
days,
hours,
minutes,
seconds
};
}
return this;
}
CountdownUtil.setTimeout = function(callback, num) {
var _startTime = new Date().getTime();
var _count = 0;
var _timer;
customSetTimeout(num);
function customSetTimeout(interval) {
_timer = setTimeout(function() {
clearTimeout(_timer);
// 执行轮询回调
callback && callback();
_count++;
var _endTime = new Date().getTime();
// 当前差值 = 轮询的当前时间 - (最初时间 + 间隔时间 * 轮询次数)
var _diff = _endTime - (_startTime + num * _count);
if (_diff < 0) {
_diff = 0;
}
console.log(`diff: ${_diff}`);
const next = num - _diff
customSetTimeout(next === 0 ? num : next);
}, interval);
}
return _timer;
}
CountdownUtil.clearTimeout = function(timer) {
clearTimeout(timer);
}
浏览器的"休眠"
关于这一点处理,其实更多的跟业务相关,不同的业务可能有不一样的处理方式。
举个例子:
- 网页实现一个 5 天的倒计时功能
- 倒计时的剩余数通过请求获取,初始为432000(s),也就是 5 天,并且服务器端也会进行一个倒计时
前端其实只是负责一个倒计时UI的显示,能让用户感知到有这么一回事,真正倒计时的还是放在了服务器端。
前面看到了切换 tab,或者网页最小化时,有延迟,那么我们只要监听用户什么时候回到页面,这个时候再去请求服务器端最新的剩余时间,重新开始倒计时,修正造成的延迟。
利用 visibilitychange 事件,切换 Tab 页以及浏览器最小化时倒计时误差修正。
// 处理页面可见属性的改变
document.addEventListener('visibilityChange', () => {
if (!document.hidden) {
// 获取服务端新的倒计时
}
});
任务队列
JavaScript 语言是单线程,同一时间只能做一件事。单线程就意味着所有任务需要排队,前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。
任务分为两种:
同步任务:在主线程上排队执行的任务,只有前一个执行完毕,才会执行下一个
异步任务:不进入主线程,而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程
事件循环:主线程从“任务队列”中读取事件,这个过程是循环不断的,所以整个的运行机制又称 Event Loop。
为什么使用 setTimeout 实现倒计时,而不是 setInterval?
setInterval
指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterva
l 指定每 100ms
执行一次,每次执行需要 5ms
,那么第一次执行结束后 95ms
,第二次执行就会开始。如果某次执行耗时特别长,比如需要 105ms
,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间有固定的间隔,可以不用 setInterval
,而是每次执行结束后,使用 setTimeout
指定下一次执行的具体时间。
为什么 setTimeout 会出现误差
setTimeout
作为异步任务,在实现倒计时功能的时候,除了执行我们功能的实现代码,还会有主线程对任务队列的读取及执行等过程,这些过程也需要耗费一些时间,所以会因为 Event Loop
的机制出现些许误差。
相关DEMO:地址
积硅步,至千里。
公众号码不停指,欢迎关注。