定时器秒杀的坑

第一种场景

定时器被暂停是因为浏览器将页面的线程停止了,毕竟浏览器已经被切到后台,为了性能考虑,所以将页面线程停止也是合理的,这就导致我们的定时器并被暂停,

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body id="body">
    <P id="text">距离19:00整</P>
    <P id="beginTime"></P>
    <P id="end"></P>
    <p>------------------------------------------------</p>
    <p id="text2"></p>
    <script src="./js/index.js"></script>
</body>

</html>
let text = document.getElementById("text")
//场景1 浏览器切到后台,页面的定时器就被暂停了,重新打开浏览器时,倒计时才继续执行,这就导致倒计时执行时长变长了
//bug
// let interval = setInterval(() => {
//     let date = new Date();
//     let year = date.getFullYear();
//     let month = date.getMonth() + 1;
//     let day = date.getDate();
//     let endDate = new Date(`${year} / ${month} /${day} 19:00`).getTime()

//     if (endDate - date.getTime() >= 0) {
//         let runTime = endDate - date.getTime();
//         let time = toHHmmss(runTime)
//         text.innerHTML = time
//     } else {
//         clearInterval(interval)
//     }
// }, 1000)

//     let date = new Date();
//     let year = date.getFullYear();
//     let month = date.getMonth() + 1;
//     let day = date.getDate();
//     let endDate = new Date(`${year} / ${month} /${day} 19:00`).getTime()

function intervals() {
    let date = new Date();
    let year = date.getFullYear();
    let month = date.getMonth() + 1;
    let day = date.getDate();
    let endDate = new Date(`${year} / ${month} /${day} 19:00`).getTime()
    watchTimeInterval(1000 * 10, 1000, () => {
        text.innerHTML = toHHmmss(endDate - new Date().getTime())
    }, () => {
        text.innerHTML = ' interval end';
    })
}
intervals()



//场景1方案解决
/**
 * @description
 * 倒计时-计时器-浏览器进程切后台后,去除进程暂停时间
 * @param {number} time 倒计时时长,单位毫秒
 * @param {number} point 倒计时间隔
 * @param {function} func 倒计时执行函数
 * @param {function} timeOverFunc 倒计时结束执行函数
 * @returns {TimeOut} 倒计时唯一标识
 * @example
 * Utils.watchTimeInterval(10*1000, 1000, () => {}, () => {})
 */
//剩余倒计时时间 - 现在时间 - 开始时间 - 间隔时间
function watchTimeInterval(time, point, func, timeOverFunc) {
    let _time = time;
    let startTime = new Date().getTime();
    let interval = setInterval(() => {
        //获取定时器执行了多长时间
        let gap = new Date().getTime() - startTime - point;
        if (gap < 0) {
            gap = 0;
        }
        //还剩多长时间
        _time = _time - gap;
        startTime = new Date().getTime();
        if (_time > 0) {
            func && func();
            _time -= point;
        } else {
            interval && clearInterval(interval);
            timeOverFunc && timeOverFunc()
        }

    }, point)
    return interval
}


function toHHmmss(time) {
    let hours = parseInt((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    let minute = parseInt((time % (1000 * 60 * 60)) / (1000 * 60));
    let second = parseInt(time % (1000 * 60) / 1000);

    return `${hours}:${minute}:${second}`;
}

 

第二种场景

js中的异步事件是通过一个循环队列来实现的,定时器的回调函数会进入到宏队列中,等待被执行,所以定时器的执行时间并不是百分百准确的,如果主线程被阻塞(我们这里暂时先不考虑这种情况)或者循环队列有多个任务,或其中有耗时的操作,那么定时器就会慢慢变得有误差。

再回到场景中,页面中同时存在多个定时器,就意味着循环队列中会同时存在多个回调函数在等待执行,若回调函数中有一些同步的数据请求或耗时的时间计算等,在页面打开的前一小段时间也许看不出来,但当页面打开较长时间,累积的误差越来越大,若用户已经打开页面,就等着倒计时结束抢购,结果等页面倒计时结束时,抢购早已开始,用户反手就是一个投诉啊。

导致这一问题的根本原因就在于同时存在的定时器太多了,生成的回调事件都在排队等着执行。减少定时器的数量就是我们需要解决的问题。

我采用的办法便是:采用观察者模式来实现。

观察者模式是一对多的设计,多个订阅者在观察者中添加订阅,观察者发现变化,通知相应的订阅者,如下简图:

 

watch

 

 

/**
 * @description
 * 为解决页面中同时存在多个倒计时的情况下,生成多个计时器导致计时出现偏差的问题。
 * 采用观察者模式,由一个定时器控制多个倒计时事件
 * @class SuspendTimeNotify
 */
class SuspendTimeNotify {
    constructor(params) {
        const { intervalPoint = 200 } = params || {};
        this._currentTime = new Date().getTime(); // 定时器回调函数执行的时间点
        this._passTime = 0; //已经执行的时长
        this.observers = [];//订阅者列表
        this._interval = null;//订阅者id
        this._intervalPoint = intervalPoint; //订阅者时间间隔
    }

    /**
     * 添加订阅者
     * @param {Object} observer 
     */
    attach(observer) {
        let item = {
            key: `${this.observers.length}_key`,
            target: observer
        }
        this.observers.push(item);
    }
    /**
     * 停止观察者倒计时
     */
    stop() {
        this.observers = [];
        this._interval && clearInterval(this._interval)
    }
    /**
     * 通知订阅者,订阅者通过 update 返回是否还继续订阅,若为 false ,则从订阅者队列中删除
     */
    notifyObserver() {
        let deleteKeys = '';
        console.log(this.observers)
        for (const { key, target } of this.observers) {
            let result = target.update(this._passTime);
            if (result) {
                deleteKeys = `${key},`
            }
        }
        if (deleteKeys) {
            this.observers = this.observers.filter(({ key }) => deleteKeys.indexOf(key) < 0)
        }
    }
    /**
     * 启动倒计时
     */
    start() {
        if (this._interval) {
            clearInterval(this._interval);
        }
        this._interval = setInterval(() => {
            let _nowTime = Date.now();
            this._passTime += _nowTime - this._currentTime;
            this._currentTime = _nowTime;
            console.log("执行");
            this.notifyObserver();
        }, this._intervalPoint)
    }
}


/**
 * 定时器订阅者
 */

class SuspendTimeObserve {
    /**
     * 
     * @param {Object} item 业务对象 通过 run 方法获取定时器执行回调 
     * @param {number} countdownTime 需要倒计时的总时长,单位毫秒
     */
    constructor(item, countdownTime) {
        this.item = item;
        this.countdownTime = countdownTime
    }

    /**
     * 接收观察者的通知事件
     * @param {number} passTime  已经执行的时长,单位毫秒
     * @returns {boolean} 是否继续订阅
     */
    update(passTime) {
        //剩余时间
        var leftCountdownTime = this.countdownTime - passTime;
        this.item.run && this.item.run({ leftCountdownTime, passTime })
        console.log(`倒计时总时长 ${this.countdownTime},执行时长${passTime},剩余时长${leftCountdownTime}`)
        return leftCountdownTime <= 0
    }
}


/**
 * 业务对象
 */
class Plan {
    constructor(i,time) {
        this.time = time;

    }
    run({ leftCountdownTime }) {
        if (leftCountdownTime > 0) {
            let dom = document.getElementById("text2")
            dom.innerHTML = toHHmmss(new Date("2020/04/13 14:22").getTime() - new Date().getTime()   )
        }
    }
    getTime() { return this.time }
}

// 创建观察者
const suspendTimeNotify = new SuspendTimeNotify({ intervalPoint: 1000 });
let runTime = new Date("2020/04/13 14:22").getTime() - new Date().getTime()
for (let i = 1; i < 7; i++) {
    const plan = new Plan(i, runTime)
    // 由业务对象创建订阅者
    const ob = new SuspendTimeObserve(plan, plan.getTime())
    // 添加订阅者
    suspendTimeNotify.attach(ob)
}
// 启动定时器
suspendTimeNotify.start()



function toHHmmss(time) {
    let hours = parseInt((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    let minute = parseInt((time % (1000 * 60 * 60)) / (1000 * 60));
    let second = parseInt(time % (1000 * 60) / 1000);

    return `${hours}:${minute}:${second}`;
}

 

其实做了一段时间,突然叫搞可能又忘记了,所以做个积累做个笔记,以后遇到直接用就是了

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值