阿里面試:寫一個(gè)倒計(jì)時(shí)功能刷掉了80% 的人
共 6910字,需瀏覽 14分鐘
·
2024-05-10 08:50
Excerpt
本文將探討如何實(shí)現(xiàn)高性能、穩(wěn)定且準(zhǔn)確的倒計(jì)時(shí)器。我們將深入分析定時(shí)器API的選擇、事件循環(huán)的影響、以及時(shí)間同步技術(shù),提供最佳實(shí)踐和代碼示例,確保倒計(jì)時(shí)的精確性和效率。
純標(biāo)題黨?。?!,但確實(shí)是阿里的大佬自己群里說(shuō)的在面試時(shí)候必問(wèn)的一個(gè)題目,其實(shí)這個(gè)問(wèn)題不僅是在面試中,也在我們的業(yè)務(wù)里也會(huì)經(jīng)常用到,所以才會(huì)寫這么一篇文章,那么到底如何才能寫一個(gè)完美的倒計(jì)時(shí)呢?
首先我們?cè)趯懙褂?jì)時(shí)的時(shí)候必須要考慮到兩點(diǎn):準(zhǔn)確性、性能。接下來(lái)我們來(lái)一步一步實(shí)現(xiàn)一個(gè)準(zhǔn)確的定時(shí)器。
setInterval:
我們先來(lái)簡(jiǎn)單實(shí)現(xiàn)一個(gè)倒計(jì)時(shí)的函數(shù):
function example1(leftTime) { let t = leftTime; setInterval(() => { t = t - 1000; console.log(t); }, 1000); } example1(10);
可以看到使用 setInterval 即可,但是 setInterval 真的準(zhǔn)確嗎?我們來(lái)看一下 MDN 中的說(shuō)明:
?? 如果你的代碼邏輯執(zhí)行時(shí)間可能比定時(shí)器時(shí)間間隔要長(zhǎng),建議你使用遞歸調(diào)用了 setTimeout()[1] 的具名函數(shù)。例如,使用 setInterval() 以 5 秒的間隔輪詢服務(wù)器,可能因網(wǎng)絡(luò)延遲、服務(wù)器無(wú)響應(yīng)以及許多其他的問(wèn)題而導(dǎo)致請(qǐng)求無(wú)法在分配的時(shí)間內(nèi)完成。
簡(jiǎn)單來(lái)說(shuō)意思就是,js 因?yàn)槭菃尉€程的原因,如果前面有阻塞線程的任務(wù),那么就可能會(huì)導(dǎo)致 setInterval 函數(shù)延遲,這樣倒計(jì)時(shí)就肯定會(huì)不準(zhǔn)確,建議使用 setTimeout 替換 setInterval。
setTimeout:
按照上述的建議將 setInterval 換為 setTimeout 后,我們來(lái)看下代碼:
function example2(leftTime) { let t = leftTime; setTimeout(() => { t = t - 1000; if (t > 0) { console.log(t); example2(t); } console.log(t); }, 1000); }
MDN 中也說(shuō)了,有很多因素會(huì)導(dǎo)致 setTimeout 的回調(diào)函數(shù)執(zhí)行比設(shè)定的預(yù)期值更久,比如嵌套超時(shí)、非活動(dòng)標(biāo)簽超時(shí)、追蹤型腳本的節(jié)流、超時(shí)延遲等等,詳情見developer.mozilla.org/zh-CN/docs/…[2],總就就是和 setInterval 差不多,時(shí)間一長(zhǎng),就會(huì)有誤差出現(xiàn),而且 setTimeout有一個(gè)很不好的點(diǎn)在于,當(dāng)你的程序在后臺(tái)運(yùn)行時(shí),setTimeout也會(huì)一直執(zhí)行,這樣會(huì)嚴(yán)重的而浪費(fèi)性能,那么有什么辦法可以解決這種問(wèn)題嗎?
requestAnimationFrame
這里就不得不提一個(gè)新的方法 requestAnimationFrame,它是一個(gè)瀏覽器 API,允許以 60 幀/秒 (FPS) 的速率請(qǐng)求回調(diào),而不會(huì)阻塞主線程。通過(guò)調(diào)用 requestAnimationFrame 方法瀏覽器會(huì)在下一次重繪之前執(zhí)行指定的函數(shù),這樣可以確保回調(diào)在每一幀之間都能夠得到適時(shí)的更新。
我們使用 requestAnimationFrame 結(jié)合 setTimeout 來(lái)優(yōu)化一下之前的代碼:
function example4(leftTime) { let t = leftTime; function start() { requestAnimationFrame(() => { t = t - 1000; setTimeout(() => { console.log(t); start(); }, 1000); }); } start(); }
為什么要使用 requestAnimationFrame + setTimeout呢?一個(gè)是息屏或者切后臺(tái)的操作時(shí),requestAnimationFrame 是不會(huì)繼續(xù)調(diào)用函數(shù)的,但是如果只使用requestAnimationFrame 的話,函數(shù)相當(dāng)于 1 秒的時(shí)候要調(diào)用 60 次,太浪費(fèi)性能。
在切后臺(tái)或者息屏的實(shí)際執(zhí)行時(shí)會(huì)發(fā)現(xiàn),當(dāng)回到頁(yè)面時(shí),倒計(jì)時(shí)會(huì)接著切后臺(tái)時(shí)的時(shí)間執(zhí)行,而沒有更新到最新的時(shí)間,這樣的bug是接受不了的。
diffTime差值計(jì)算:
要解決上述的問(wèn)題,最通用的辦法就是通過(guò)時(shí)間差值每次進(jìn)行對(duì)比就可以了。
function example5(leftTime) { const now = performance.now(); function start() { setTimeout(() => { const diff = leftTime - (performance.now() - now); console.log(diff); requestAnimationFrame(start); }, 1000); } start(); }
上面的代碼實(shí)現(xiàn)思路其實(shí)在實(shí)際的業(yè)務(wù)中已經(jīng)能夠滿足我們的使用場(chǎng)景,但其實(shí)還是沒有解決setTimeout會(huì)延遲的問(wèn)題,當(dāng)線程被占用之后,很容易出現(xiàn)誤差,那么有什么更新的辦法進(jìn)行處理呢?
最佳方案
先要明確的是,setTimeout函數(shù)中執(zhí)行代碼的時(shí)間肯定是要大于等于setTimeout時(shí)間的,那么就可能出現(xiàn)設(shè)定的 1 秒,實(shí)際執(zhí)行卻執(zhí)行了 2 秒的情況,那么我們的實(shí)現(xiàn)思路也很簡(jiǎn)單,每次計(jì)算一下setTimeout實(shí)際執(zhí)行的時(shí)間,然后動(dòng)態(tài)的調(diào)整下一次執(zhí)行的時(shí)間,而不是設(shè)置固定的值
我們來(lái)用圖表舉例推演一下每次執(zhí)行的情況:
| 第n次執(zhí)行 | executionTime 實(shí)際執(zhí)行時(shí)間 | nextTime 下次需要執(zhí)行的時(shí)間 | totleTime 執(zhí)行的總時(shí)間 |
|---|---|---|---|
| 0 | 0 | 1000 | 0 |
| 1 | 1200 | 800 | 1200 |
| 2 | 1100 | 700 | 2300 |
| 3 | 1000 | 700 | 3300 |
| 4 | 2200 | 500 | 5500 |
| 5 | 1300 | 200 | 6800 |
| 6 | 1200 | 1000 | 8000 |
| … | … | … | … |
從中可以看到:下次執(zhí)行的時(shí)間 nextTime = 1000 - totleTime % 1000;這樣我們就可以得出下次執(zhí)行的時(shí)間,從而每次都去動(dòng)態(tài)的調(diào)整多余消耗的時(shí)間,大大減小倒計(jì)時(shí)最終的誤差
還有需要考慮的是,實(shí)際業(yè)務(wù)中返回的剩余時(shí)間肯定不會(huì)是整數(shù),所以我們的第一次執(zhí)行的時(shí)間最好可以先讓剩余時(shí)間變?yōu)檎麛?shù),這樣可以在倒計(jì)時(shí)到最后一秒時(shí)更加的精確。
根據(jù)上述的思路來(lái)看一下最終封裝出來(lái)的 react hooks:
const useCountDown = ({ leftTime, ms = 1000, onEnd }) => { const countdownTimer = useRef(); const startTimer = useRef(); //記錄初始時(shí)間 const startTimeRef = useRef(performance.now()); // 第一次執(zhí)行的時(shí)間處理,讓下一次倒計(jì)時(shí)時(shí)調(diào)整為整數(shù) 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 = performance.now(); // 算出每次實(shí)際執(zhí)行的時(shí)間 const executionTime = currentTime - startTimeRef.current; // 實(shí)際執(zhí)行時(shí)間大于上一次需要執(zhí)行的時(shí)間,說(shuō)明執(zhí)行時(shí)間多了,否則需要補(bǔ)上差的時(shí)間 const diffTime = executionTime > nextTimeRef.current ? executionTime - nextTimeRef.current : nextTimeRef.current - executionTime; setCount((count) => { const nextCount = count - (Math.floor(executionTime / ms) || 1) * ms - nt; return nextCount <= 0 ? 0 : nextCount; }); // 算出下一次的時(shí)間 nextTimeRef.current = executionTime > nextTimeRef.current ? ms - diffTime : ms + diffTime; // 重置初始時(shí)間 startTimeRef.current = performance.now(); 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;
如果想要封裝組件的話,可以在hooks的基礎(chǔ)上進(jìn)行二次封裝。
到這里,肯定會(huì)有人說(shuō),做了這么多的操作,有必要嗎,就算差0點(diǎn)幾秒,在實(shí)際體驗(yàn)中用戶完全感受不出來(lái)。我想說(shuō)的是,細(xì)節(jié)決定成敗,有可能這零點(diǎn)幾秒的內(nèi)容就決定了面試的成敗。如果做什么事都只做個(gè)差不多,那你永遠(yuǎn)不會(huì)有自己的"核心科技"。關(guān)注細(xì)節(jié),從中去學(xué)一些解題的思路或者方法,然后積累沉淀,才能讓自己持續(xù)成長(zhǎng)。
除了上述的優(yōu)化思路,歡迎大家有更好的想法也可以隨時(shí)進(jìn)行探討~ 歡迎大家關(guān)注我的博客:www.lpeakcc.com/[3]
原文: https://juejin.cn/post/7343921389084426277
作者:大橘為重07
https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout: https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FsetTimeout
[2]https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout: https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FsetTimeout
[3]https://link.juejin.cn/?target=https%3A%2F%2Fwww.lpeakcc.com%2F: https://link.juejin.cn/?target=https%3A%2F%2Fwww.lpeakcc.com%2F
