JS面試必備之如何實現(xiàn)一個精確的倒計時
又到了金三銀四的季節(jié)了,面試的各位同學(xué)要開始準(zhǔn)備起來了,今天主要分享一個在面試中經(jīng)常被提到的一個面試題:倒計時。其實這個問題不僅是在面試中,也在我們的業(yè)務(wù)里也會經(jīng)常用到,所以到底如何才能寫好一個倒計時呢?
首先我們在寫倒計時的時候必須要考慮到三點:準(zhǔn)確性、穩(wěn)定性、性能。接下來我們來一步一步實現(xiàn)一個準(zhǔn)確的定時器。
setInterval
我們先來簡單實現(xiàn)一個倒計時的函數(shù):
function example1(leftTime){
let t = leftTime;
setInterval(() => {
t = t - 1000;
console.log(t);
}, 1000);
}
example1(10);
可以看到使用setInterval即可,但是setInterval真的準(zhǔn)確嗎?我們來看一下MDN中的說明:
如果你的代碼邏輯執(zhí)行時間可能比定時器時間間隔要長,建議你使用遞歸調(diào)用了setTimeout 的具名函數(shù)。例如,使用 setInterval() 以 5 秒的間隔輪詢服務(wù)器,可能因網(wǎng)絡(luò)延遲、服務(wù)器無響應(yīng)以及許多其他的問題而導(dǎo)致請求無法在分配的時間內(nèi)完成。
簡單來說意思就是,js因為是單線程的原因,如果前面有阻塞線程的任務(wù),那么就可能會導(dǎo)致setInterval函數(shù)延遲,這樣倒計時就肯定會不準(zhǔn)確,建議使用setTimeout替換setInterval。
setTimeout
按照上述的建議將setInterval換為setTimeout后,我們來看下代碼:
function example2(leftTime) {
let t = leftTime;
setTimeout(() => {
t = t - 1000;
if (t > 0) {
console.log(t);
example2(t);
}
console.log(t);
}, 1000);
}
MDN中也說了,有很多因素會導(dǎo)致 setTimeout 的回調(diào)函數(shù)執(zhí)行比設(shè)定的預(yù)期值更久,比如嵌套超時、非活動標(biāo)簽超時、追蹤型腳本的節(jié)流、超時延遲等等,詳情見developer.mozilla.org/zh-CN/docs/Web/API/setTimeout,總就就是和setInterval差不多,都可能會有延遲執(zhí)行的時候,這么一來如何提高倒計時的準(zhǔn)確性呢?
requestAnimationFrame
這里就不得不提一個新的方法requestAnimationFrame,它是一個瀏覽器 API,允許以 60 幀/秒 (FPS) 的速率請求回調(diào),而不會阻塞主線程。通過調(diào)用 requestAnimationFrame 方法瀏覽器會在下一次重繪之前執(zhí)行指定的函數(shù),這樣可以確?;卣{(diào)在每一幀之間都能夠得到適時的更新。
我們使用requestAnimationFrame結(jié)合setTimeout來優(yōu)化一下之前的代碼:
function example3(leftTime) {
let t = leftTime;
function start() {
requestAnimationFrame(() => {
t = t - 1000;
setTimeout(() => {
console.log(t);
start();
}, 1000);
});
}
start();
}
這樣的話就可以保證我們倒計時的穩(wěn)定性,使用requestAnimationFrame還有一個好處就是,息屏或者切后臺的操作時,requestAnimationFrame是不會繼續(xù)調(diào)用函數(shù)的,但是如果只使用setTimeout或者setInterval的話,他們會在后臺一直執(zhí)行,顯而易見,requestAnimationFrame更加的節(jié)省性能開銷。
在切后臺或者息屏的實際執(zhí)行時會發(fā)現(xiàn),雖然性能上好了,但上述的方法在準(zhǔn)確性上確出了問題,當(dāng)回到頁面時,倒計時會接著切后臺時的時間執(zhí)行,這樣肯定是不對。
diffTime
要解決上述的問題,通過時間差值每次進(jìn)行對比就可以了。
function example4(leftTime) {
const now = new Date().getTime();
function start() {
requestAnimationFrame(() => {
const diff = leftTime - (new Date().getTime() - now);
setTimeout(() => {
console.log(diff);
start();
}, 1000);
});
}
start();
}上面的代碼實現(xiàn)思路其實在實際的業(yè)務(wù)中已經(jīng)能夠滿足我們的使用場景,但是如果想要在面試的時候表現(xiàn)的再好一點其實還是有可以優(yōu)化空間的,那么還要怎么優(yōu)化呢?
finally

我們來仔細(xì)分析一下,如上圖所示,上面代碼每次在setTimeout時其實真正執(zhí)行的時間不可能完全是一秒,可能多也可能少,這樣時間一長之后執(zhí)行到結(jié)束時肯定會和實際時間有誤差,雖然在秒級的單位不一定能看出來,如果采用毫秒做單位的話肯定會有些許。那么應(yīng)該如何處理呢?
其實最終的實現(xiàn)思路有也很簡單,在每次在執(zhí)行setTimeout的時候,不要將每次setTimeout的時間都設(shè)置為1000ms,而是算出每次執(zhí)行實際setTimeout中執(zhí)行的函數(shù)話費的時間,根據(jù)實際執(zhí)行的時間算出下次setTimeout需要執(zhí)行的時間即可。至于下次執(zhí)行的時間應(yīng)該怎么算呢?我們來通過簡單的一個圖表來找出其中的規(guī)律,假設(shè)下圖是每次setTimeout執(zhí)行的時間分布圖所示:
| Time時間 | executionTime實際執(zhí)行時間 | diffTime差值 | nextTime下次執(zhí)行時間 |
|---|---|---|---|
| 0 | 1200 | 200 | 1000 |
| 1000 | 900 | 100 | 900 |
| 2000 | 900 | 0 | 1000 |
| 3000 | 800 | 200 | 1200 |
| 4000 | 1300 | 100 | 900 |
| … | … | … | … |
通過上面的圖表我們可以發(fā)現(xiàn) nextTime = executionTime > nextTime ? 1000 - diffTime : 1000 + diffTime。
還需要注意的時候,一般在實際業(yè)務(wù)時后端返回給我們剩余時間字段,通常都不會是整秒的,這樣我們第一次執(zhí)行setTimeout的時的執(zhí)行時間就需要處理一下,在倒計時最后結(jié)束時可以大大減少最終的時間誤差。
根據(jù)上述的思路我們來看一下最終封裝出來的react hooks:
const useCountDown = ({ leftTime, ms = 1000, onEnd }) => {
const countdownTimer = useRef();
const startTimer = useRef();
const startTimeRef = useRef(new Date().getTime());
const nextTimeRef = useRef(leftTime % ms);
const [count, setCount] = useState(leftTime);
const clearTimer = () => {
countdownTimer.current && clearTimeout(countdownTimer.current);
startTimer.current && clearTimeout(startTimer.current);
};
const startCountDown = () => {
clearTimer();
const currentTime = new Date().getTime();
// 每次實際執(zhí)行的時間
const executionTime = currentTime - startTimeRef.current;
// 實際執(zhí)行時間大于上一次需要執(zhí)行的時間,說明執(zhí)行時間多了,差值為多出來的時間,否則為少了的時間
const diffTime =
executionTime > nextTimeRef.current
? executionTime - nextTimeRef.current
: nextTimeRef.current - executionTime;
// 剩余時間減去應(yīng)該執(zhí)行的時間
setCount((count) => {
const c = count - (count % ms === 0 ? ms : count % ms);
if (c <= 0) return 0;
return c;
});
// 算出下一次的時間 思路:本次的實際執(zhí)行時間>下一次執(zhí)行的時間 ?1000 - diffTime : 1000 + diffTime;
nextTimeRef.current =
executionTime > nextTimeRef.current ? ms - diffTime : ms + diffTime;
// 重置初始時間
startTimeRef.current = new Date().getTime();
countdownTimer.current = setTimeout(() => {
requestAnimationFrame(startCountDown);
}, nextTimeRef.current);
};
useEffect(() => {
setCount(leftTime);
startTimer.current = setTimeout(
startCountDown,
nextTimeRef.current,
);
return () => {
clearTimer();
};
}, [leftTime]);
useEffect(() => {
if (count <= 0) {
clearTimer();
onEnd && onEnd();
}
}, [count]);
return count;
};
export default useCountDown;
除了上述的優(yōu)化思路,歡迎大家有更好的想法也可以隨時進(jìn)行探討~
到此這篇關(guān)于JS面試必備之如何實現(xiàn)一個精確的倒計時的文章就介紹到這了,更多相關(guān)JS倒計時內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家
相關(guān)文章
Javascript結(jié)合css實現(xiàn)網(wǎng)頁換膚功能
現(xiàn)在網(wǎng)站換皮膚是比較常見的功能,大多數(shù)論壇都有的,要想實現(xiàn)這樣效果可以看如下代碼.2009-11-11
JavaScript將數(shù)組轉(zhuǎn)換為鏈表的方法
這篇文章主要介紹了JavaScript將數(shù)組轉(zhuǎn)換為鏈表的方法,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02
javaScript 刪除確認(rèn)實現(xiàn)方法小結(jié)
因為對于內(nèi)容的刪除是件很重要的事,所以一般的系統(tǒng)中,都需要刪除確認(rèn)一下,以免誤刪,具體的方法如下,大家可以參考下。2009-12-12
javascript感應(yīng)鼠標(biāo)圖片透明度顯示的方法
這篇文章主要介紹了javascript感應(yīng)鼠標(biāo)圖片透明度顯示的方法,涉及javascript針對鼠標(biāo)事件及圖片透明度操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-02-02
javascript實現(xiàn)省市區(qū)三級聯(lián)動下拉框菜單
這篇文章主要為大家詳細(xì)介紹了javascript實現(xiàn)省市區(qū)三級聯(lián)動下拉框菜單很詳細(xì)的代碼,解決了大家實現(xiàn)javascript省市區(qū)三級聯(lián)動下拉框菜單的問題,感興趣的小伙伴們可以參考一下2015-11-11
使用JavaScript實現(xiàn)旋轉(zhuǎn)的彩圈特效
這篇文章主要介紹了使用JavaScript實現(xiàn)旋轉(zhuǎn)的彩圈特效的相關(guān)資料,需要的朋友可以參考下2015-06-06

