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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM