HarmonyOS - 自定义组件之计时器
文章目录
- 前段时间项目中遇到了计时器的功能,项目中的计时器其实只是显示功能,数据全是由设备上报的。完成项目后,自己做了一个小的计时器组件,在这个过程中也发现了一些问题。
- 组件直接传入以秒为单位的数据,最终显示如下效果:
-
- 正常情况下,说到计时器首先想到的是使用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,来规避掉上面的缺点。
- 当我们在使用计时工具的时候,因为一些原因,将浏览器退到后台,再次进来的时候,发现计时器时间不对,感觉刚才退出去的这段时间,计时器是停止状态。针对这个问题,查询会发现,出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), 会将 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)},
- 这是容易忽略的一点,我们的组件唯一的一个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, },};
- 这个计时器组件就是对日常工作的发散,作为一个鸿蒙小白,顺便做一个小小的练习。 想了解更多关于开源的内容,请访问: 51CTO 开源基础软件社区 https://ost.51cto.com。

前段时间项目中遇到了计时器的功能,项目中的计时器其实只是显示功能,数据全是由设备上报的。完成项目后,自己做了一个小的计时器组件,在这个过程中也发现了一些问题。
组件直接传入以秒为单位的数据,最终显示如下效果:

正常情况下,说到计时器首先想到的是使用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,来规避掉上面的缺点。
当我们在使用计时工具的时候,因为一些原因,将浏览器退到后台,再次进来的时候,发现计时器时间不对,感觉刚才退出去的这段时间,计时器是停止状态。针对这个问题,查询会发现,出于节能的考虑, 部分浏览器在进入后台时(或者失去焦点时), 会将 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)
},
这是容易忽略的一点,我们的组件唯一的一个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,
},
};
这个计时器组件就是对日常工作的发散,作为一个鸿蒙小白,顺便做一个小小的练习。