<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    阿里面試:寫一個(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(leftTimelet t = leftTime; setInterval(() => { t = t - 1000console.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(leftTimelet t = leftTime; setTimeout(() => { t = t - 1000if (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(leftTimelet 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(leftTimeconst 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


    [1]

    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


    瀏覽 121
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    成人美女在线视频 | 免费成人A V电影 | 欧美日韩一级片免费看 | 黄色一级片日韩学生妹无套无码内射视频 | 日日操夜夜叫狠狠插 |