• 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

  • 相关阅读:
    iOS开发之视频播放
    iOS开发之Copy & MutableCopy及深复制 & 浅复制
    iOS开发之JSON & XML
    iOS开发之NSObject的多线程
    iOS开发之单例模式
    iOS开发之Run Loop
    taro开发微信小程序-页面开发规范
    视频Video放器的部分实例方法
    Input框搜索关键字高亮显示
    vue上拉加载下拉加载
  • 原文地址:https://www.cnblogs.com/cnsyear/p/12732023.html
Copyright © 2020-2023  润新知