zoukankan      html  css  js  c++  java
  • JS倒计时客户端和服务器时间同步问题

    JS倒计时客户端和服务器时间同步问题

    需求实现考试时间页面倒计时。

    这个需求以前在刀具大赛的时候也遇到过,当时是使用前端每秒定时请求后台返回倒计时时间。这样的缺点就是当用户量大的时候,会有的大量的请求造成性能下降(其实用户少或者使用场景少的时候也没啥事),优点就是时间比较准确,没有浏览器的兼容问题。

    还有一种解决方案就是第一次请求的时候返回时间,然后就在客户端倒计时就好(当然为了防止客户端改时间作弊,提交请求的时间要在服务器端检查)。这种做的优点服务端没有请求的压力,实现起来也比较简单。

    一、存在问题的实现方式:

    复制粘贴拿起键盘,啪啪啪 倒计时代码就好了

    var time = 60;//服务端返回的剩余时间
        
    var set = setInterval(function() {
    	time--;
    	console.log(time)
    	if(time === 0) {
    		clearInterval(set);
    	}
    }, 1000);
    
    执行结果
     59
     58
     57
    省略其他。。。
    

    存在的问题:你这东西不准啊,我看着几分钟,有好几秒的延迟

    其实是setTimeout/setInterval误差的问题,我们可通过减少误差,通过对下一次任务的调用时间进行修正。

    代码如下:

    let count = 0;
    let countdown = 5000; //服务器返回的倒计时时间
    let interval = 1000;
    let startTime = new Date().getTime();
    let timer = setTimeout(countDownStart, interval); //首次执行
    //定时器测试
    function countDownStart() {
        count++;
        const offset = new Date().getTime() - (startTime + count * 1000);
        const nextInterval = interval - offset; //修正后的延时时间
        if (nextInterval < 0) {
            nextInterval = 0;
        }
        countdown -= interval;
        console.log("误差:" + offset + "ms,下一次执行:" + nextInterval + "ms后,离活动开始还有:" + countdown + "ms");
        if (countdown <= 0) {
            clearTimeout(timer);
        } else {
            timer = setTimeout(countDownStart, nextInterval);
        }
    }
    
    
    执行结果
     误差:11ms,下一次执行:989ms后,离活动开始还有:4000ms
     误差:4ms,下一次执行:996ms后,离活动开始还有:3000ms
     误差:2ms,下一次执行:998ms后,离活动开始还有:2000ms
     误差:4ms,下一次执行:996ms后,离活动开始还有:1000ms
     误差:9ms,下一次执行:991ms后,离活动开始还有:0ms
    省略其他。。。
    

    存在的问题:你这东西有问题啊,浏览器切换网页后,在回来看页面,这段过程是暂停的,延迟了几分钟 没考虑浏览器的"休眠",浏览器切换回来,倒计时是暂停的

    综上所述:

    浏览器中的定时器任务是有误差的,也就是我们常说的 setTimeout 为什么不准的问题,这里涉及到 js 单线程以及运行机制,具体运行原理可参考 2019-11-04-JS倒计时setTimeout为什么会出现误差

    二、优化后的实现方式:

    即使利用setTimeout()模拟setInterval(),还是会因为其余脚本的执行,造成误差。所以,我认为JS定时函数setInterval、setTimeout的弊端无法避免,只能通过多次与服务器沟通,来矫正时间。

    封装后的countDown.js

    (function () {
        function timer(delay) {
            console.log('timer' + delay);
            var self = this;
            this._queue = [];
            setInterval(function () {
                    for (var i = 0; i < self._queue.length; i++) {
                        self._queue[i]();
                    }
                },
                delay);
        }
    
        timer.prototype = {
            constructor: timer,
            add: function (cb) {
                this._queue.push(cb);
                return this._queue.length - 1;
            },
            remove: function (index) {
                this._queue.splice(index, 1);
            }
        };
    
        var delayTime = 1000;
    
        var msInterval = new timer(delayTime);
    
        function countDown(config) {
            //默认配置
            var defaultOptions = {
                fixNow: 3 * 1000,
                fixNowDate: true,
                now: new Date().valueOf(),
                template: '{d}:{h}:{m}:{s}',
                render: function (outstring) {
                    console.log(outstring);
                },
                end: function () {
                    console.log('the end!');
                },
                endTime: new Date().valueOf() + 5 * 1000 * 60
            };
            for (var i in defaultOptions) {
                this[i] = config[i] || defaultOptions[i];
            }
            this.init();
        }
    
        countDown.prototype = {
            constructor: countDown,
            init: function () {
                console.log('countDown init');
                var self = this;
                //是否开启服务器时间校验
                if (this.fixNowDate) {
                    var fix = new timer(this.fixNow);
                    fix.add(function () {
                        self.getNowTime(function (now) {
                            console.log('服务器时间校准,' + self.now + '----------' + now);
                            self.now = now;
                        });
                    });
                }
                //倒计时
                var index = msInterval.add(function () {
                    self.now += delayTime;
                    if (self.now >= self.endTime) {
                        msInterval.remove(index);
                        self.end();
                    } else {
                        self.render(self.getOutString());
                    }
                });
            },
            getBetween: function () {
                return _formatTime(this.endTime - this.now);
            },
            getOutString: function () {
                var between = this.getBetween();
                return this.template.replace(/{(w*)}/g, function (m, key) {
                    return between.hasOwnProperty(key) ? between[key] : "";
                });
            },
            getNowTime: function (cb) {
                var xhr = new XMLHttpRequest();
                xhr.open('get', '/', true);
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 3) {
                        var now = xhr.getResponseHeader('Date');
                        cb(new Date(now).valueOf());
                    }
                };
                xhr.send(null);
            }
        };
    
        function _cover(num) {
            var n = parseInt(num, 10);
            return n < 10 ? '0' + n : n;
        }
    
        function _formatTime(ms) {
            var s = ms / 1000,
                m = s / 60;
            return {
                d: _cover(m / 60 / 24),
                h: _cover(m / 60 % 24),
                m: _cover(m % 60),
                s: _cover(s % 60)
            };
        }
    
        var now = Date.now();
    
        //new countDown({});
    
        window.$countDown = countDown;
    
    })();
    

    使用方法

    首先要引入countDown.js

     //倒计时10秒
    new window.$countDown({
        fixNow: 3 * 1000, //3秒一次服务器时间校准
        template: '{d}天{h}:{m}:{s}',
        render: function (outstring) {
            console.log(outstring);
            if (outstring.indexOf('00天') > -1) {
                outstring = outstring.substring(3);
            }
            $("#timebox").text(outstring);
        },
        end: function () {
            console.log('the end!');
        },
        endTime: new Date().valueOf() + 10 * 1000 * 60 //时间戳
    });
    
    

    经测试通过服务器时间校准,可以避免时间不准的问题而且还大大减轻了服务器端的压力。即使浏览器切到后台运行,倒计时停止也没有关系。

    参考文档:

    https://segmentfault.com/q/1010000000698541/a-1020000000698620

    https://juejin.im/post/5bcd89d5e51d4579bb1c5e22

    https://www.zhihu.com/question/28896402

  • 相关阅读:
    Java实现 LeetCode 27 移除元素
    Java实现 LeetCode 26 删除排序数组中的重复项
    Java实现 LeetCode 26 删除排序数组中的重复项
    Java实现 LeetCode 26 删除排序数组中的重复项
    Java实现 LeetCode 25 K个一组翻转链表
    Java实现 LeetCode 25 K个一组翻转链表
    Java实现 LeetCode 25 K个一组翻转链表
    Java实现 LeetCode 24 两两交换链表中的节点
    Java实现 LeetCode 24 两两交换链表中的节点
    Java实现 LeetCode 24 两两交换链表中的节点
  • 原文地址:https://www.cnblogs.com/cnsyear/p/12732023.html
Copyright © 2011-2022 走看看