实现较为精准倒计时的三种方式

2,278 阅读1分钟

前言:想起前段时间面试的时候面试官问了一个问题,如何实现一个较为精准的倒计时。当时就根据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);
		       }
		 }

无线程代码占用执行时间:

1.png 有线程代码占用执行时间:

image.png 这里会有个场景,假设项目的其他代码执行占据线程好几次超过了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>

4.jpg

5.png

总结:一个定时器没想到收获这么多,了解了动画帧,了解了webwork。本人还是前端小菜鸡,写的不好请多多包涵,也请各路大神能多给点意见(* ̄︶ ̄)