天天看點

#夏日挑戰賽# 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開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。

繼續閱讀