第一种场景
定时器被暂停是因为浏览器将页面的线程停止了,毕竟浏览器已经被切到后台,为了性能考虑,所以将页面线程停止也是合理的,这就导致我们的定时器并被暂停,
<!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中的异步事件是通过一个循环队列来实现的,定时器的回调函数会进入到宏队列中,等待被执行,所以定时器的执行时间并不是百分百准确的,如果主线程被阻塞(我们这里暂时先不考虑这种情况)或者循环队列有多个任务,或其中有耗时的操作,那么定时器就会慢慢变得有误差。
再回到场景中,页面中同时存在多个定时器,就意味着循环队列中会同时存在多个回调函数在等待执行,若回调函数中有一些同步的数据请求或耗时的时间计算等,在页面打开的前一小段时间也许看不出来,但当页面打开较长时间,累积的误差越来越大,若用户已经打开页面,就等着倒计时结束抢购,结果等页面倒计时结束时,抢购早已开始,用户反手就是一个投诉啊。
导致这一问题的根本原因就在于同时存在的定时器太多了,生成的回调事件都在排队等着执行。减少定时器的数量就是我们需要解决的问题。
我采用的办法便是:采用观察者模式来实现。
观察者模式是一对多的设计,多个订阅者在观察者中添加订阅,观察者发现变化,通知相应的订阅者,如下简图:
/**
* @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}`;
}
其实做了一段时间,突然叫搞可能又忘记了,所以做个积累做个笔记,以后遇到直接用就是了