<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>

          字節(jié)面試官問粉絲,如何實(shí)現(xiàn)準(zhǔn)時(shí)的setTimeout

          共 9450字,需瀏覽 19分鐘

           ·

          2021-05-01 10:48




          最近一個(gè)粉絲去面字節(jié),被面試官問到了這個(gè)問題來問我,一聽感覺有點(diǎn)意思,于是對它進(jìn)行了一番研究,可能研究的過程以及結(jié)果不一定是最好的,但是還是記錄一下,為各位提供一些幫助。

          拿到這個(gè)問題,假設(shè)有這樣的場景,我們需要用 setTimeout 做一個(gè)動(dòng)畫,并且需要控制他的頻率,50ms 運(yùn)行一次,首先我們先上圖,來看看 setTimeout 的表現(xiàn)。

          運(yùn)行代碼如下,通過一個(gè)計(jì)數(shù)器來記錄每一次 setTimeout 的調(diào)用,而設(shè)定的間隔 * 計(jì)數(shù)次數(shù),就等于理想狀態(tài)下的延遲,通過以下例子來查看我們計(jì)時(shí)器的準(zhǔn)確性。

          function timer({
             var speed = 50// 設(shè)定間隔
             counter = 1,  // 計(jì)數(shù)
             start = new Date().getTime();
             
             function instance()
             
          {
              var ideal = (counter * speed),
              real = (new Date().getTime() - start);
              
              counter++;
              form.ideal.value = ideal; // 記錄理想值
              form.real.value = real;   // 記錄真實(shí)值

              var diff = (real - ideal);
              form.diff.value = diff;  // 差值

              window.setTimeout(function({ instance(); }, speed);
             };
             
             window.setTimeout(function({ instance(); }, speed);
          }
          timer();

          而我們?nèi)绻?setTimeout 還未執(zhí)行期間加入一些額外的代碼邏輯,再來看看這個(gè)差值。

          ...
          window.setTimeout(function({ instance(); }, speed);
          for(var x=1, i=0; i<10000000; i++) { x *= (i + 1); }
          }
          ...

          可以看出,這大大加劇了誤差。

          可以看到隨著時(shí)間的推移, setTimeout 實(shí)際執(zhí)行的時(shí)間和理想的時(shí)間差值會(huì)越來越大,這就不是我們預(yù)期的樣子。類比真實(shí)的場景,對于一些倒計(jì)時(shí)以及動(dòng)畫來說都會(huì)造成時(shí)間的偏差都是不理想的。

          那么,從這個(gè)現(xiàn)象來看一下,為什么 setTimeout 會(huì)不準(zhǔn)時(shí)呢?

          因?yàn)槲覀兊拇a往往并不是只有一個(gè) setTimeout,大多數(shù)會(huì)遇到以下情況。

          詳細(xì)要從瀏覽器的事件循環(huán)講起,但是講事件循環(huán)的文章太多了,文本就不再累贅地詳細(xì)展開講解。

          視頻

          • https://www.youtube.com/watch?v=8aGhZQkoFbQ

          (國內(nèi)視頻 https://www.bilibili.com/video/av456657611/)

          建議看國外的中英對照字幕,國內(nèi)的翻譯準(zhǔn)確度一般

          相關(guān)文章

          • https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

          • 極客時(shí)間 - 李兵 - 15 | 消息隊(duì)列和事件循環(huán):頁面是怎么“活”起來的?https://time.geekbang.org/column/article/134456

          總結(jié)來說,因?yàn)闉g覽器頁面是有消息隊(duì)列和事件循環(huán)來驅(qū)動(dòng)的,創(chuàng)建一個(gè) setTimeout 的時(shí)候是將它推進(jìn)了一個(gè)隊(duì)列,并沒有立即執(zhí)行,只有本輪宏任務(wù)執(zhí)行完,才會(huì)去檢查當(dāng)前的消息隊(duì)列是否有有到期的任務(wù)。

          接下來我會(huì)用 4 這種方式來探索。

          while

          想得到準(zhǔn)確的,我們第一反應(yīng)就是如果我們能夠主動(dòng)去觸發(fā),獲取到最開始的時(shí)間,以及不斷去輪詢當(dāng)前時(shí)間,如果差值是預(yù)期的時(shí)間,那么這個(gè)定時(shí)器肯定是準(zhǔn)確的,那么用 while 可以實(shí)現(xiàn)這個(gè)功能。

          理解起來也很簡單:

          i

          代碼如下:

          function timer(time{
              const startTime = Date.now();
              while(true) {
                  const now = Date.now();
                  if(now - startTime >= time) {
                      console.log('誤差', now - startTime - time);
                      return;
                  }
              }
          }
          timer(5000);

          打印:誤差 0

          顯然這樣的方式很精確,但是我們知道 js 是單線程運(yùn)行,使用這樣的方式強(qiáng)行霸占線程會(huì)使得頁面進(jìn)入卡死狀態(tài),這樣的結(jié)果顯然是不合適的。

          Web Worker

          那么既然無法在當(dāng)前主線程避免這個(gè)誤差,我們能否另開一個(gè)線程去處理呢?當(dāng)然可以,JavaScript 也提供給我們這樣一個(gè)能力,通過 Web Worker 我們就可以在另一個(gè)線程來運(yùn)行我們的代碼。

          Web Worker為Web內(nèi)容在后臺線程中運(yùn)行腳本提供了一種簡單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面。              -- 摘自MDN

          一個(gè) worker 的簡單的示例

          // main.js
          var myWorker = new Worker('worker.js');

          // 監(jiān)聽 worker
          myWorker.onmessage = function(e{
            result.textContent = e.data;
            console.log('Message received from worker');
          }
          first.onchange = function({
            // 向 worker 發(fā)送數(shù)據(jù)
            myWorker.postMessage([first.value,second.value]);
            console.log('Message posted to worker');
          }

          // worker.js
          onmessage = function(e{
            // 接受主線程的數(shù)據(jù)
            console.log('Message received from main script');
            var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
            console.log('Posting message back to main script');
            // 向主線程發(fā)送數(shù)據(jù)
            postMessage(workerResult);
          }

          那么接下來我們就要加 worker 和 while 相結(jié)合,以下為創(chuàng)建 worker 部分

          // worker生成器
          const createWorker = (fn, options) => {
              const blob = new Blob(['(' + fn.toString() + ')()']);
              const url = URL.createObjectURL(blob);
              if (options) {
                  return new Worker(url, options);
              }
              return new Worker(url);

          // worker 部分
          const worker = createWorker(function ({
              onmessage = function (e{
                  const date = Date.now();
                  while (true) {
                      const now = Date.now();
                      if(now - date >= e.data) {
                          postMessage(1);
                          return;
                      }
                  }
              }
          })

          我們通過在 worker 中寫入一個(gè) while 循環(huán),當(dāng)達(dá)到我們的預(yù)取時(shí)間的時(shí)候,再向主線程發(fā)送一個(gè)完成事件,就不會(huì)因?yàn)橹骶€程的其他代碼的干擾而造成數(shù)據(jù)不準(zhǔn)的情況。

          let isStart = false;
          function timer({
              worker.onmessage = function (e{
                 cb()
                  if (isStart) {
                      worker.postMessage(speed);
                  } 
              }
              worker.postMessage(speed);
          }

          我們來看一下實(shí)際的效果。

          我們可以看到執(zhí)行的時(shí)間和理想的時(shí)間非常相近,而那細(xì)微的差異應(yīng)該就是線程通訊耗時(shí)。

          我們再來看看加入額外的代碼邏輯的情況。

          ...
          if (isStart) {
             worker.postMessage(speed);
          }
          for (var x = 1, i = 0; i < 10000000; i++) { x *= (i + 1); }
          ...

          ![](https://s3.qiufengh.com/blog/2021-04-20 23.16.44.gif)

          時(shí)間明顯增加了一些,但是增加速度非常緩慢。

          雖然我們用 Web Worker 修復(fù)時(shí)間看似被解決了。但是一方面, worker 線程會(huì)被 while 給占用,導(dǎo)致無法接受到信息,多個(gè)定時(shí)器無法同時(shí)執(zhí)行,另一方面,由于 onmessage 還是屬于事件循環(huán)內(nèi),如果主線程有大量阻塞還是會(huì)讓時(shí)間越差越大,因此這并不是個(gè)完美的方案。

          requestAnimationFrame

          先來看看他的定義

          window.requestAnimationFrame() 告訴瀏覽器——你希望執(zhí)行一個(gè)動(dòng)畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動(dòng)畫。該方法需要傳入一個(gè)回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會(huì)在瀏覽器下一次重繪之前執(zhí)行,回調(diào)函數(shù)執(zhí)行次數(shù)通常是每秒60次,也就是每16.7ms 執(zhí)行一次,但是并不一定保證為 16.7 ms。

          我們也可以嘗試一下將它來模擬 setTimeout

          // 模擬代碼
          function setTimeout2 (cb, delay{
              let startTime = Date.now()
              loop()
            
              function loop ({
                const now = Date.now()
                if (now - startTime >= delay) {
                  cb();
                  return;
                }
                requestAnimationFrame(loop)
              }
          }

          發(fā)現(xiàn)由于 16.7 ms 間隔執(zhí)行,在使用間隔很小的定時(shí)器,很容易導(dǎo)致時(shí)間的不準(zhǔn)確。

          再看看額外代碼的引入效果。

          ...
           window.setInterval2(function ({ instance(); }, speed);
          }
          for (var x = 1, i = 0; i < 10000000; i++) { x *= (i + 1); }
          ...

          略微加劇了誤差的增加,因此這種方案仍然不是一種好的方案。

          setTimeout 系統(tǒng)時(shí)間補(bǔ)償

          這個(gè)方案是在 stackoverflow 看到的一個(gè)方案,我們來看看此方案和原方案的區(qū)別

          原方案

          setTimeout系統(tǒng)時(shí)間補(bǔ)償

          當(dāng)每一次定時(shí)器執(zhí)行時(shí)后,都去獲取系統(tǒng)的時(shí)間來進(jìn)行修正,雖然每次運(yùn)行可能會(huì)有誤差,但是通過系統(tǒng)時(shí)間對每次運(yùn)行的修復(fù),能夠讓后面每一次時(shí)間都得到一個(gè)補(bǔ)償。

          function timer({
             var speed = 500,
             counter = 1
             start = new Date().getTime();
             
             function instance()
             
          {
              var ideal = (counter * speed),
              real = (new Date().getTime() - start);
              
              counter++;

              var diff = (real - ideal);
              form.diff.value = diff;

              window.setTimeout(function({ instance(); }, (speed - diff)); // 通過系統(tǒng)時(shí)間進(jìn)行修復(fù)

             };
             
             window.setTimeout(function({ instance(); }, speed);
          }

          再來看看加入額外的代碼邏輯的情況。

          依舊非常的穩(wěn)定,因此通過系統(tǒng)的時(shí)間補(bǔ)償,能夠讓我們的 setTimeout 變得更加準(zhǔn)時(shí),至此我們完成了如何讓 setTimeout 準(zhǔn)時(shí)的探索。

          好了我們最后來總結(jié)一下4種方案的優(yōu)缺點(diǎn)


          whileWeb WorkerrequestAnimationFramesetTimeout 系統(tǒng)時(shí)間補(bǔ)償
          準(zhǔn)確度
          主線程阻塞阻塞一般不阻塞不阻塞
          評分??????????????????????

          我們下期再見~

          參考

          https://segmentfault.com/q/1010000013909430

          https://stackoverflow.com/questions/196027/is-there-a-more-accurate-way-to-create-a-javascript-timer-than-settimeout



          瀏覽 101
          點(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>
                  小视频+福利 | 中文精品无码视频 | 插BB免费看 | 色爽无码 | 91操碰|