前言:想起前段时间面试的时候面试官问了一个问题,如何实现一个较为精准的倒计时。当时就根据js的单线程机制和事件循环机制回答根据时间差来调整定时器。最后面试官跟我说这个方向还挺有趣的,你可以再去了解一下。后面花了几天时间学习了一波,写一篇文章记录下。
setTimeout实现倒计时
对于定时器,大家一定会想到两个,setTimeout和setInterval,这里为什么使用setTimeout而不是另一个,我去网上查阅一番,setInterval可能存在两个问题:1.时间间隔或许会跳过 2.时间间隔可能<定时调用的代码的执行时间.具体情况我也不是很了解,希望知道的大佬能否告知下
这里先分析一下从获取服务器时间到前端显示倒计时的过程:
1.客户端http请求服务器时间
2.服务器响应完成
3.服务器通过网络传输时间数据到客户端
4.客户端根据活动开始时间和服务器时间差做倒计时显示
服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差了,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十ms以内,大的可能有几百ms。
//继续线程占用
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++;
//offset是误差,即startTime是最开始的时间,现在的时间减去startTime和已经需要几次倒计时的时间得出误差的时间
var offset = new Date().getTime() - (startTime + count * interval);
//这里是获取下次以倒计时所需的时间,如果倒计时为1秒,误差为100ms,则下次倒计时为900ms
var nextTime = interval - offset;
var daytohour = 0;
//如果误差为小于0说明倒计时结束
if (nextTime < 0) { nextTime = 0 };
ms -= interval;//这里是正确倒计时的话,说明还有多少秒结束
console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
if(ms < 0){
//如果小于0则清除掉倒计时
clearTimeout(timeCounter);
}else{
//否则重新设置倒计时,间隔时间为计算出来减去误差的时间
timeCounter = setTimeout(countDownStart,nextTime);
}
}
无线程代码占用执行时间:
有线程代码占用执行时间:
这里会有个场景,假设项目的其他代码执行占据线程好几次超过了500ms以上,那么你下次可能0.5s或者立即执行,页面就会展示出数字连续跳动。这时候我们应该做一个判断,如果误差高于500ms(这个值自己来设定),那么我下一次定时器时间恒定为800ms,中间差值跟下一次误差累计。以此循环。将过大误差平分到每一次的定时器中。只不过治标不治本,还是建议找到阻塞严重的原因,优化代码。
webwork实现精准倒计时
WebWork 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。具体感兴趣的小伙伴可以自行去了解下。简单来说webwork允许用户另开线程来进行复杂的逻辑计算处理,且不会收到主线程的影响。
注意:dom是不能被异步操作的! WebWork不能在本地使用
<html>
<head>
<title></title>
</head>
<body>
<div id="hangMe">
定时
</div>
<div id="timer"></div>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
$(function () {
//代码会自动生成这个文件
let worker = new Worker('lib/worker.js');
worker.addEventListener('message', function (e) {
console.log('message', e.data)
});
let remain = 5 * 1000; // 倒计时剩余时间 ms
let ele = $('#timer');
let delay = 1000;
let start = +new Date();
let timer;
showTime(remain);
timer = setTimeout(countdown, delay);
function countdown() {
let current = +new Date();
let offset = current - start;
console.log('误差值', offset)
remain -= offset;
start = current;
if (remain <= 0) {
clearTimeout(timer);
return false;
}
showTime(remain);
timer = setTimeout(countdown, delay);
}
function showTime(time) {
const hour = Math.floor(time / 1000 / 60 / 60 % 24).toString().padStart(2, '0');
const min = Math.floor(time / 1000 / 60 % 60).toString().padStart(2, '0');
const sec = Math.floor(time / 1000 % 60).toString().padStart(2, '0');
ele.text(hour + ":" + min + ":" + sec);
}
//给webwork线程传递消息
$('#hangMe').on('click', _ => {
worker.postMessage('hang');
});
})
</script>
</body>
</html>
//webwork.js
self.addEventListener('message', function(e) {
console.log(e.data);
if(e.data === 'hang') {
let i = 0;
let then = Date.now()
while (true) {
var now = Date.now()
if (now - then > 1000) {
if (i++ >= 3) {
break;
}
then = now;
console.log(i);
}
}
}
//继续线程占用
setInterval(function(){
var j = 0;
while(j++ < 100000000);
}, 0);
onmessage= function(e){
console.log('e',e)
}
// 与主线程通信
postMessage('我是work线程');
});
在webwork.js里面线程占用代码无论注释还是运行,对于主线程那边的代码影响都不大,倒计时误差值都是很小的。
requestAnimationFrame实现倒计时
requestAnimationFrame
requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。简单来说,如果你的屏幕刷新率是60hz,那么就是每1000/60≈16.7ms运行一次函数.
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
1、requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
2、在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
<html>
<head><title>动画帧实现倒计时</title></head>
<body>
<div id="text" style="width: 100px;height: 100px;">11</div>
<script>
const totalDuration = 5 * 1000;
let requestRef = null;
let startTime;
let prevEndTime;
let prevTime;
let currentCount = totalDuration;
let endTime;
let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
let interval = 1000;
let nextTime = interval;
let count = 0;
setInterval(() => {
let n = 0;
while (n++ < 1000000000);
}, 0);
const animate = (timestamp) => {
//这是最最开始的时间
// console.log('timestamp'+timestamp)
if (prevTime !== undefined) {
//deltaTime其实就是每一帧大约16ms的总和,每一次相减得出的值就是16ms的倍数的积。
const deltaTime = timestamp - prevTime;
console.log('daltaTime:'+deltaTime)
//一直到动画帧积累到了超过1000ms然后开始执行定时器的效果
if (deltaTime >= nextTime) {
prevTime = timestamp;
//preEndTime为最开始运行也就是最开始倒计时的时间
prevEndTime = endTime;
endTime = new Date().getTime();
//把ms换算为s
currentCount = currentCount - 1000;
console.log("currentCount: ", currentCount / 1000);
timeDifferance = endTime - startTime - (totalDuration - currentCount);
console.log('倒计时偏差:'+timeDifferance);
nextTime = interval - timeDifferance;
// 慢太多了,就立刻执行下一个循环
if (nextTime < 0) {
nextTime = 0;
}
console.log(`执行下一次渲染的时间是:${nextTime}ms`);
document.getElementById('text').innerText=currentCount/1000
console.log(count++)
if (currentCount <= 0) {
currentCount = 0;
cancelAnimationFrame(requestRef);
console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
return;
}
}
} else {
startTime = new Date().getTime();
prevTime = timestamp;
endTime = new Date().getTime();
console.log('else:'+prevTime)
console.log('startTime:'+startTime)
}
//若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用
//回调函数执行次数通常与浏览器屏幕刷新次数相匹配
requestRef = requestAnimationFrame(animate);
};
requestRef = requestAnimationFrame(animate);
</script>
</body>
</html>
总结:一个定时器没想到收获这么多,了解了动画帧,了解了webwork。本人还是前端小菜鸡,写的不好请多多包涵,也请各路大神能多给点意见(* ̄︶ ̄)