天天看点

#夏日挑战赛# HarmonyOS - 自定义组件之计时器

作者: 张悦

本文正在参加星光计划3.0–夏日挑战赛

前言

前段时间项目中遇到了计时器的功能,项目中的计时器其实只是显示功能,数据全是由设备上报的。完成项目后,自己做了一个小的计时器组件,在这个过程中也发现了一些问题。

效果展示

组件直接传入以秒为单位的数据,最终显示如下效果:

#夏日挑战赛# HarmonyOS - 自定义组件之计时器

实现原理

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代码,所以并没有达到定时器的效果。

#夏日挑战赛# HarmonyOS - 自定义组件之计时器

综上所述,setInterval有两个缺点:

  1. 使用setInterval时,某些间隔会被跳过;
  2. 可能多个定时器会连续执行;

所以,我们要使用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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

继续阅读