作者: 張悅
本文正在參加星光計劃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開發技術,歡迎投稿和訂閱,讓我們一起攜手前行共建鴻蒙生态。