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

          如何實(shí)現(xiàn)比 setTimeout 快 80 倍的定時(shí)器?

          共 8326字,需瀏覽 17分鐘

           ·

          2021-05-17 14:34

          起因

          很多人都知道,setTimeout 是有最小延遲時(shí)間的,根據(jù) MDN 文檔 setTimeout:實(shí)際延時(shí)比設(shè)定值更久的原因:最小延遲時(shí)間[1] 中所說(shuō):

          在瀏覽器中,setTimeout()/setInterval() 的每調(diào)用一次定時(shí)器的最小間隔是 4ms,這通常是由于函數(shù)嵌套導(dǎo)致(嵌套層級(jí)達(dá)到一定深度)。

          HTML Standard[2] 規(guī)范中也有提到更具體的:

          Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.

          簡(jiǎn)單來(lái)說(shuō),5 層以上的定時(shí)器嵌套會(huì)導(dǎo)致至少 4ms 的延遲。

          用如下代碼做個(gè)測(cè)試:

          let a = performance.now();
          setTimeout(() => {
            let b = performance.now();
            console.log(b - a);
            setTimeout(() => {
              let c = performance.now();
              console.log(c - b);
              setTimeout(() => {
                let d = performance.now();
                console.log(d - c);
                setTimeout(() => {
                  let e = performance.now();
                  console.log(e - d);
                  setTimeout(() => {
                    let f = performance.now();
                    console.log(f - e);
                    setTimeout(() => {
                      let g = performance.now();
                      console.log(g - f);
                    }, 0);
                  }, 0);
                }, 0);
              }, 0);
            }, 0);
          }, 0);

          在瀏覽器中的打印結(jié)果大概是這樣的,和規(guī)范一致,第五次執(zhí)行的時(shí)候延遲來(lái)到了 4ms 以上。

          更詳細(xì)的原因,可以參考 為什么 setTimeout 有最小時(shí)延 4ms ?

          探索

          假設(shè)我們就需要一個(gè)「立刻執(zhí)行」的定時(shí)器呢?有什么辦法繞過(guò)這個(gè) 4ms 的延遲嗎,上面那篇 MDN 文檔的角落里有一些線索:

          如果想在瀏覽器中實(shí)現(xiàn) 0ms 延時(shí)的定時(shí)器,你可以參考這里[3]所說(shuō)的 window.postMessage()

          這篇文章里的作者給出了這樣一段代碼,用 postMessage 來(lái)實(shí)現(xiàn)真正 0 延遲的定時(shí)器:

          (function ({
            var timeouts = [];
            var messageName = 'zero-timeout-message';

            // 保持 setTimeout 的形態(tài),只接受單個(gè)函數(shù)的參數(shù),延遲始終為 0。
            function setZeroTimeout(fn{
              timeouts.push(fn);
              window.postMessage(messageName, '*');
            }

            function handleMessage(event{
              if (event.source == window && event.data == messageName) {
                event.stopPropagation();
                if (timeouts.length > 0) {
                  var fn = timeouts.shift();
                  fn();
                }
              }
            }

            window.addEventListener('message', handleMessage, true);

            // 把 API 添加到 window 對(duì)象上
            window.setZeroTimeout = setZeroTimeout;
          })();

          由于 postMessage 的回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)和 setTimeout 類(lèi)似,都屬于宏任務(wù),所以可以簡(jiǎn)單利用 postMessageaddEventListener('message') 的消息通知組合,來(lái)實(shí)現(xiàn)模擬定時(shí)器的功能。

          這樣,執(zhí)行時(shí)機(jī)類(lèi)似,但是延遲更小的定時(shí)器就完成了。

          再利用上面的嵌套定時(shí)器的例子來(lái)跑一下測(cè)試:

          全部在 0.1 ~ 0.3 毫秒級(jí)別,而且不會(huì)隨著嵌套層數(shù)的增多而增加延遲。

          測(cè)試

          從理論上來(lái)說(shuō),由于 postMessage 的實(shí)現(xiàn)沒(méi)有被瀏覽器引擎限制速度,一定是比 setTimeout 要快的。但空口無(wú)憑,咱們用數(shù)據(jù)說(shuō)話。

          作者設(shè)計(jì)了一個(gè)實(shí)驗(yàn)方法,就是分別用 postMessage 版定時(shí)器和傳統(tǒng)定時(shí)器做一個(gè)遞歸執(zhí)行計(jì)數(shù)函數(shù)的操作,看看同樣計(jì)數(shù)到 100 分別需要花多少時(shí)間。讀者也可以在這里自己跑一下測(cè)試[4]

          實(shí)驗(yàn)代碼:

          function runtest({
            var output = document.getElementById('output');
            var outputText = document.createTextNode('');
            output.appendChild(outputText);
            function printOutput(line{
              outputText.data += line + '\n';
            }

            var i = 0;
            var startTime = Date.now();
            // 通過(guò)遞歸 setZeroTimeout 達(dá)到 100 計(jì)數(shù)
            // 達(dá)到 100 后切換成 setTimeout 來(lái)實(shí)驗(yàn)
            function test1({
              if (++i == 100) {
                var endTime = Date.now();
                printOutput(
                  '100 iterations of setZeroTimeout took ' +
                    (endTime - startTime) +
                    ' milliseconds.'
                );
                i = 0;
                startTime = Date.now();
                setTimeout(test2, 0);
              } else {
                setZeroTimeout(test1);
              }
            }

            setZeroTimeout(test1);

            // 通過(guò)遞歸 setTimeout 達(dá)到 100 計(jì)數(shù)
            function test2({
              if (++i == 100) {
                var endTime = Date.now();
                printOutput(
                  '100 iterations of setTimeout(0) took ' +
                    (endTime - startTime) +
                    ' milliseconds.'
                );
              } else {
                setTimeout(test2, 0);
              }
            }
          }

          實(shí)驗(yàn)代碼很簡(jiǎn)單,先通過(guò) setZeroTimeout 也就是 postMessage 版本來(lái)遞歸計(jì)數(shù)到 100,然后切換成 setTimeout 計(jì)數(shù)到 100。

          直接放結(jié)論,這個(gè)差距不固定,在我的 mac 上用無(wú)痕模式排除插件等因素的干擾后,以計(jì)數(shù)到 100 為例,大概有 80 ~ 100 倍的時(shí)間差距。在我硬件更好的臺(tái)式機(jī)上,甚至能到 200 倍以上。

          Performance 面板

          只是看冷冰冰的數(shù)字還不夠過(guò)癮,我們打開(kāi) Performance 面板,看看更直觀的可視化界面中,postMessage 版的定時(shí)器和 setTimeout 版的定時(shí)器是如何分布的。

          這張分布圖非常直觀的體現(xiàn)出了我們上面所說(shuō)的所有現(xiàn)象,左邊的 postMessage 版本的定時(shí)器分布非常密集,大概在 5ms 以?xún)?nèi)就執(zhí)行完了所有的計(jì)數(shù)任務(wù)。

          而右邊的 setTimeout 版本相比較下分布的就很稀疏了,而且通過(guò)上方的時(shí)間軸可以看出,前四次的執(zhí)行間隔大概在 1ms 左右,到了第五次就拉開(kāi)到 4ms 以上。

          作用

          也許有同學(xué)會(huì)問(wèn),有什么場(chǎng)景需要無(wú)延遲的定時(shí)器?其實(shí)在 React 的源碼中,做時(shí)間切片的部分就用到了。

          借用 React Scheduler 為什么使用 MessageChannel 實(shí)現(xiàn)[5] 這篇文章中的一段偽代碼:

          const channel = new MessageChannel();
          const port = channel.port2;

          // 每次 port.postMessage() 調(diào)用就會(huì)添加一個(gè)宏任務(wù)
          // 該宏任務(wù)為調(diào)用 scheduler.scheduleTask 方法
          channel.port1.onmessage = scheduler.scheduleTask;

          const scheduler = {
            scheduleTask() {
              // 挑選一個(gè)任務(wù)并執(zhí)行
              const task = pickTask();
              const continuousTask = task();

              // 如果當(dāng)前任務(wù)未完成,則在下個(gè)宏任務(wù)繼續(xù)執(zhí)行
              if (continuousTask) {
                port.postMessage(null);
              }
            },
          };

          React 把任務(wù)切分成很多片段,這樣就可以通過(guò)把任務(wù)交給 postMessage 的回調(diào)函數(shù),來(lái)讓瀏覽器主線程拿回控制權(quán),進(jìn)行一些更優(yōu)先的渲染任務(wù)(比如用戶(hù)輸入)。

          為什么不用執(zhí)行時(shí)機(jī)更靠前的微任務(wù)呢?參考我的這篇對(duì) EventLoop 規(guī)范的解讀 深入解析 EventLoop 和瀏覽器渲染、幀動(dòng)畫(huà)、空閑回調(diào)的關(guān)系,關(guān)鍵的原因在于微任務(wù)會(huì)在渲染之前執(zhí)行,這樣就算瀏覽器有緊急的渲染任務(wù),也得等微任務(wù)執(zhí)行完才能渲染。

          總結(jié)

          通過(guò)本文,你大概可以了解如下幾個(gè)知識(shí)點(diǎn):

          1. setTimeout 的 4ms 延遲歷史原因,具體表現(xiàn)。
          2. 如何通過(guò) postMessage 實(shí)現(xiàn)一個(gè)真正 0 延遲的定時(shí)器。
          3. postMessage 定時(shí)器在 React 時(shí)間切片中的運(yùn)用。
          4. 為什么時(shí)間切片需要用宏任務(wù),而不是微任務(wù)。

          參考資料

          [1]

          MDN 文檔 setTimeout:實(shí)際延時(shí)比設(shè)定值更久的原因:最小延遲時(shí)間: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#%E5%AE%9E%E9%99%85%E5%BB%B6%E6%97%B6%E6%AF%94%E8%AE%BE%E5%AE%9A%E5%80%BC%E6%9B%B4%E4%B9%85%E7%9A%84%E5%8E%9F%E5%9B%A0%EF%BC%9A%E6%9C%80%E5%B0%8F%E5%BB%B6%E8%BF%9F%E6%97%B6%E9%97%B4

          [2]

          HTML Standard: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers

          [3]

          這里: https://dbaron.org/log/20100309-faster-timeouts

          [4]

          這里自己跑一下測(cè)試: https://dbaron.org/mozilla/zero-timeout

          [5]

          React Scheduler 為什么使用 MessageChannel 實(shí)現(xiàn): https://juejin.cn/post/6953804914715803678




          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專(zhuān)業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  国产精品无码专区AV在线播放 | AV55 | 免费高清AV在线看 | 国产中文字幕免费观看 | 免费人妻视频 | 大雞巴弄得我好舒服黃片 |