<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          阿里面試:寫一個(gè)倒計(jì)時(shí)功能刷掉了80% 的人

          共 10077字,需瀏覽 21分鐘

           ·

          2024-05-10 22:41

          原文: https://juejin.cn/post/7343921389084426277

          作者: 大橘為重07

          純標(biāo)題黨!!!,但確實(shí)是阿里的大佬自己群里說的在面試時(shí)候必問的一個(gè)題目,其實(shí)這個(gè)問題不僅是在面試中,也在我們的業(yè)務(wù)里也會經(jīng)常用到,所以才會寫這么一篇文章,那么到底如何才能寫一個(gè)完美的倒計(jì)時(shí)呢?

          首先我們在寫倒計(jì)時(shí)的時(shí)候必須要考慮到三點(diǎn):準(zhǔn)確性、性能。接下來我們來一步一步實(shí)現(xiàn)一個(gè)準(zhǔn)確的定時(shí)器。

          setInterval:

          我們先來簡單實(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)確嗎?我們來看一下 MDN 中的說明:

          ?? 如果你的代碼邏輯執(zhí)行時(shí)間可能比定時(shí)器時(shí)間間隔要長,建議你使用遞歸調(diào)用了 setTimeout()[1] 的具名函數(shù)。例如,使用 setInterval() 以 5 秒的間隔輪詢服務(wù)器,可能因網(wǎng)絡(luò)延遲、服務(wù)器無響應(yīng)以及許多其他的問題而導(dǎo)致請求無法在分配的時(shí)間內(nèi)完成。

          簡單來說意思就是,js 因?yàn)槭菃尉€程的原因,如果前面有阻塞線程的任務(wù),那么就可能會導(dǎo)致 setInterval 函數(shù)延遲,這樣倒計(jì)時(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ù)期值更久,比如嵌套超時(shí)、非活動(dòng)標(biāo)簽超時(shí)、追蹤型腳本的節(jié)流、超時(shí)延遲等等,詳情見developer.mozilla.org/zh-CN/docs/…[2],總就就是和 setInterval 差不多,時(shí)間一長,就會有誤差出現(xiàn),而且 setTimeout有一個(gè)很不好的點(diǎn)在于,當(dāng)你的程序在后臺運(yùn)行時(shí),setTimeout也會一直執(zhí)行,這樣會嚴(yán)重的而浪費(fèi)性能,那么有什么辦法可以解決這種問題嗎?

          requestAnimationFrame

          這里就不得不提一個(gè)新的方法 requestAnimationFrame,它是一個(gè)瀏覽器 API,允許以 60 幀/秒 (FPS) 的速率請求回調(diào),而不會阻塞主線程。通過調(diào)用 requestAnimationFrame 方法瀏覽器會在下一次重繪之前執(zhí)行指定的函數(shù),這樣可以確保回調(diào)在每一幀之間都能夠得到適時(shí)的更新。

          我們使用 requestAnimationFrame 結(jié)合 setTimeout 來優(yōu)化一下之前的代碼:

          function example4(leftTime{
              let t = leftTime;
              function start({
                  requestAnimationFrame(() => {
                      t = t - 1000;
                      setTimeout(() => {
                          console.log(t);
                          start();
                      }, 1000);
                  });
              }
              start();
          }

          為什么要使用 requestAnimationFrame + setTimeout呢?一個(gè)是息屏或者切后臺的操作時(shí),requestAnimationFrame 是不會繼續(xù)調(diào)用函數(shù)的,但是如果只使用requestAnimationFrame 的話,函數(shù)相當(dāng)于 1 秒的時(shí)候要調(diào)用 60 次,太浪費(fèi)性能。

          在切后臺或者息屏的實(shí)際執(zhí)行時(shí)會發(fā)現(xiàn),當(dāng)回到頁面時(shí),倒計(jì)時(shí)會接著切后臺時(shí)的時(shí)間執(zhí)行,而沒有更新到最新的時(shí)間,這樣的bug是接受不了的。

          diffTime差值計(jì)算:

          要解決上述的問題,最通用的辦法就是通過時(shí)間差值每次進(jìn)行對比就可以了。

          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)能夠滿足我們的使用場景,但其實(shí)還是沒有解決setTimeout會延遲的問題,當(dāng)線程被占用之后,很容易出現(xiàn)誤差,那么有什么更新的辦法進(jìn)行處理呢?

          最佳方案

          先要明確的是,setTimeout函數(shù)中執(zhí)行代碼的時(shí)間肯定是要大于等于setTimeout時(shí)間的,那么就可能出現(xiàn)設(shè)定的 1 秒,實(shí)際執(zhí)行卻執(zhí)行了 2 秒的情況,那么我們的實(shí)現(xiàn)思路也很簡單,每次計(jì)算一下setTimeout實(shí)際執(zhí)行的時(shí)間,然后動(dòng)態(tài)的調(diào)整下一次執(zhí)行的時(shí)間,而不是設(shè)置固定的值

          我們來用圖表舉例推演一下每次執(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í)間肯定不會是整數(shù),所以我們的第一次執(zhí)行的時(shí)間最好可以先讓剩余時(shí)間變?yōu)檎麛?shù),這樣可以在倒計(jì)時(shí)到最后一秒時(shí)更加的精確。

          根據(jù)上述的思路來看一下最終封裝出來的 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í)間,說明執(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)行二次封裝。

          到這里,肯定會有人說,做了這么多的操作,有必要嗎,就算差0點(diǎn)幾秒,在實(shí)際體驗(yàn)中用戶完全感受不出來。我想說的是,細(xì)節(jié)決定成敗,有可能這零點(diǎn)幾秒的內(nèi)容就決定了面試的成敗。如果做什么事都只做個(gè)差不多,那你永遠(yuǎn)不會有自己的"核心科技"。關(guān)注細(xì)節(jié),從中去學(xué)一些解題的思路或者方法,然后積累沉淀,才能讓自己持續(xù)成長。

          除了上述的優(yōu)化思路,歡迎大家有更好的想法也可以隨時(shí)進(jìn)行探討~ 歡迎大家關(guān)注我的博客:www.lpeakcc.com/[3]


          參考資料
          [1]

          https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout: https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout

          [2]

          https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout: https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout

          [3]

          https://link.juejin.cn/?target=https%3A%2F%2Fwww.lpeakcc.com%2F: https://www.lpeakcc.com/


          瀏覽 97
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人电影久久 | 男人天堂色| 波多野结衣无码NET,AV | 国产毛片AV一区二区三区牛牛影视 | 国产精品人妻无码八区牛牛 |