作者: 张悦
本文正在参加星光计划3.0–夏日挑战赛
前言
前段时间项目中遇到了计时器的功能,项目中的计时器其实只是显示功能,数据全是由设备上报的。完成项目后,自己做了一个小的计时器组件,在这个过程中也发现了一些问题。
效果展示
组件直接传入以秒为单位的数据,最终显示如下效果:
实现原理
1. 用setTimeout模拟setInterval的行为
正常情况下,说到计时器首先想到的是使用setInterval,对比setTimeout要去重复调用,setInterval很方便就能实现,如下代码:
getTime(time) {
this.countNum = time;
setTimeout(() => {
this.getTime(time --)
}, 1000)
},
setInterval(() => {
this.countNum --
}, 1000)
但为什么要使用setTimeout呢,查下两个定时器的原理,会发现,创建一个时间间隔为100ms的定时器,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。
综上所述,setInterval有两个缺点:
- 使用setInterval时,某些间隔会被跳过;
- 可能多个定时器会连续执行;
所以,我们要使用setTimeout模拟setInterval,来规避掉上面的缺点。
2. 用Date.now()获取当前时间,规避浏览器退出再进来造成的计时误差
当我们在使用计时工具的时候,因为一些原因,将浏览器退到后台,再次进来的时候,发现计时器时间不对,感觉刚才退出去的这段时间,计时器是停止状态。针对这个问题,查询会发现,出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), 会将 setTimeout 等定时任务暂停,待用户回到浏览器时, 才会重新激活定时任务。
说是暂停,实践操作会发现其实应该说是延迟, 1s 的任务延迟到 2s, 或者更久,总之,计时器计算掉的时间,比实际过去的时间少。解决这个问题,我们可以使用Date.now()记录时间,如下,计算两次计时事件的时间差,来计算每次计时的step。
getTime(time) {
this.countNum = time;
setTimeout(() => {
const nowDate = Date.now()
const diff = Math.floor((nowDate - this.curTime) / 1000)
const step = diff > 1 ? diff : 1 // 页面退到后台后计时有偏差,对比时间差,得到计时step
this.curTime = nowDate
this.getTime(time - step)
}, 1000)
},
3. 及时清理定时器
这是容易忽略的一点,我们的组件唯一的一个prop属性就是time,实际的业务场景中,可能会有一些操作改变倒计时的时长,所以我们的组件需要监听time值的改变,来做一些初始化操作,这时候你会发现,当做了2次初始化操作后,我们的代码中会同时存在了2个计时器,时间过了1秒后2个计时器同时触发,对time值做了2次“- 1”操作,所以,我们在初始化时要清除之前的定时器
getTime(time) {
this.timer && clearTimeout(this.timer) //清除定时器
this.countNum = time;
this.timer = setTimeout(() => {
const nowDate = Date.now()
const diff = Math.floor((nowDate - this.curTime) / 1000)
const step = diff > 1 ? diff : 1 // 页面退到后台后计时有偏差,对比时间差,得到计时step
this.curTime = nowDate
this.getTime(time - step)
}, 1000)
},
实现过程
countDown组件hml部分:
<div class="count-down">
<image class="count" src="./count.png"></image>
<div class="countNumCon">
<text class="countNum">
{{countNum}}
</text>
</div>
</div>
countDown组件js部分:
export default {
props: [
'time'
],
data: {
timer: null, //定时器
curTime: 0, //记录上次操作时间
countNum: '',
},
onInit() {
this.countDown()
this.$watch('time', 'countDown');
},
//将传入的时间(s)转换成天/小时/分钟/秒
durationFormatter(time) {
if (!time) return { ss: 0 }
let t = time
const ss = t % 60
t = (t - ss) / 60
if (t < 1) return { ss }
const mm = t % 60
t = (t - mm) / 60
if (t < 1) return { mm, ss }
const hh = t % 24
t = (t - hh) / 24
if (t < 1) return { hh, mm, ss }
const dd = t
return { dd, hh, mm, ss }
},
//处理时间格式
dealTime(time){
return `00${time || ''}`.slice(-2);
},
countDown() {
this.curTime = Date.now()
this.getTime(this.time)
},
//计算时间
getTime(time) {
this.timer && clearTimeout(this.timer)
if (time < 0) {
return
}
const { dd, hh, mm, ss } = this.durationFormatter(time)
this.countNum = `${dd || 0}天 ${this.dealTime(hh)}:${this.dealTime(mm)}:${this.dealTime(ss)}`;
this.timer = setTimeout(() => {
const nowDate = Date.now()
const diff = Math.floor((nowDate - this.curTime) / 1000)
const step = diff > 1 ? diff : 1 // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时
this.curTime = nowDate
this.getTime(time - step)
}, 1000)
},
};
countDown组件css部分:
.count-down {
display: flex;
margin-top: 200px;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 50%;
}
.countImg {
width: 200px;
height: 100px;
}
.numContainer {
background-color: black;
height: 57px;
width: 140px;
left: 8px;
top: -80px;
justify-content: center;
border-radius: 7px;
}
.count {
color: white;
font-size: 22px;
font-weight: 500;
}
父组件hml部分:
<element name="countDown" src="../countDown/countDown.hml">
</element>
<div class="container">
<countDown time="{{ leftTime }}">
</countDown>
</div>
父组件js部分:
export default {
data: {
leftTime: 100,
},
};
总结
这个计时器组件就是对日常工作的发散,作为一个鸿蒙小白,顺便做一个小小的练习,可能存在一些问题,望大家指正~
更多原创内容请关注:中软国际 HarmonyOS 技术团队
入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。