欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺析JavaScript定時器setTimeout的時延問題

 更新時間:2023年11月08日 09:56:40   作者:前端技術(shù)棧  
這篇文章主要為大家詳細(xì)介紹了JavaScript中定時器setTimeout有最小時延的相關(guān)知識,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

全局的 setTimeout()  方法設(shè)置一個定時器,一旦定時器到期,就會執(zhí)行一個函數(shù)或指定的代碼片段,但是需要注意的是,setTimeout 并不是 ECMAScript 標(biāo)準(zhǔn)的一部分,不過幾乎每一個 JS 運行時都支持了這個函數(shù)。定時器的使用比較簡單,這里不再闡述,我們這篇文章主要聊下關(guān)于 setTimeout 有最小時延的相關(guān)知識。

一、為什么定時器有最小時延

其實 setTimeout 是有最小時延的,這是為什么呢?有很多因素會導(dǎo)致 setTimeout 的回調(diào)函數(shù)執(zhí)行比設(shè)定的預(yù)期值更久,這里列舉一些導(dǎo)致定時器出現(xiàn)時延的原因:

1. 函數(shù)過度嵌套

MDN 文檔:在瀏覽器中,每調(diào)用一次定時器的最小間隔是 4ms,這通常是由于函數(shù)嵌套導(dǎo)致(嵌套層級達(dá)到一定深度),5 層以上的定時器嵌套會導(dǎo)致至少 4ms 的延遲。

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í)行的時候延遲來到了 4ms 以上:

2. 非活動標(biāo)簽的超時

為了優(yōu)化后臺標(biāo)簽的加載損耗(以及降低耗電量),瀏覽器會在非活動標(biāo)簽中強制執(zhí)行一個最小的超時延遲,如果一個頁面正在使用網(wǎng)絡(luò)音頻 API 播放聲音,也可以不執(zhí)行該延遲。

這方面的具體情況與瀏覽器有關(guān):

  • Firefox 桌面版和 Chrome 針對不活動標(biāo)簽都有一個 1 秒的最小超時值
  • 安卓版 Firefox 瀏覽器對不活動的標(biāo)簽有一個至少 15 分鐘的超時,并可能完全卸載它們

3. 追蹤型腳本的節(jié)流

Firefox 對它識別為追蹤型腳本的腳本實施額外的節(jié)流,當(dāng)在前臺運行時,節(jié)流的最小延遲仍然是 4ms,然而,在后臺標(biāo)簽中,節(jié)流的最小延遲是 10000 毫秒,即 10 秒,在文檔首次加載后 30 秒開始生效。

4. 超時延遲

如果頁面(或操作系統(tǒng)/瀏覽器)正忙于其他任務(wù),超時也可能比預(yù)期的晚,需要注意的一個重要情況是,在調(diào)用 setTimeout() 的線程結(jié)束之前,函數(shù)或代碼片段不能被執(zhí)行。例如:

function foo() {
  console.log("foo 被調(diào)用");
}
setTimeout(foo, 0);
console.log("setTimeout 之后");
 
// 控制臺輸出:
// setTimeout 之后
// foo 被調(diào)用

出現(xiàn)這個結(jié)果的原因是,盡管 setTimeout 以 0ms 的延遲來調(diào)用函數(shù),但這個任務(wù)已經(jīng)被放入了隊列中并且等待下一次執(zhí)行,并不是立即執(zhí)行;隊列中的等待函數(shù)被調(diào)用之前,當(dāng)前代碼必須全部運行完畢,因此這里運行結(jié)果并非預(yù)想的那樣。

5. 在加載頁面時推遲超時

當(dāng)前標(biāo)簽頁正在加載時,F(xiàn)irefox 將推遲觸發(fā) setTimeout() 計時器,直到主線程被認(rèn)為是空閑的,類似于 window.requestIdleCallback(),或者直到加載事件觸發(fā)完畢,才開始觸發(fā)。

二、為什么定時器最小時延是4ms

我們首先要知道是不是存在具體的規(guī)范來指定了 4ms, 還是只是業(yè)界實踐的既定事實?

1. HTML standard

在 HTML standard 8.6 Timers-2020/6/23 中對于 setTimeout() 有詳細(xì)的描述,我們只看其中的 10-13 行:

If timeout is less than 0, then set timeout to 0.
If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.
Increment nesting level by one.
Let task's timer nesting level be nesting level.

從上面的規(guī)范可以看出來:

  • 如果設(shè)置的 timeout 小于 0,則設(shè)置為 0
  • 如果嵌套的層級超過了 5 層,并且 timeout 小于 4ms,則設(shè)置 timeout 為 4ms

到這里,我們似乎已經(jīng)找到了 4ms 的出處,并且對于 setTimeout 的最小延遲有了更加精確的定義 - 需要同時滿足嵌套層級超過 5 層,timeout 小于 4ms,才會設(shè)置 4ms

2. 瀏覽器源碼

除了尋找規(guī)范的出處之外,還可以去瀏覽器的源碼中尋找答案,我們進(jìn)入到谷歌瀏覽器源碼中來查找用來設(shè)置計時器延時的地方:

static const int maxIntervalForUserGestureForwarding = 1000; // One second matches Gecko.
static const int maxTimerNestingLevel = 5;
static const double oneMillisecond = 0.001;
// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static const double minimumInterval = 0.004;
double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);
if (intervalMilliseconds < minimumInterval && m_nestingLevel >= maxTimerNestingLevel)
    intervalMilliseconds = minimumInterval;

代碼邏輯很清晰,設(shè)置了幾個常量:

  • maxTimerNestingLevel = 5,也就是 HTML standard 當(dāng)中提到的嵌套層級
  • minimumInterval = 0.004,也就是 HTML standard 當(dāng)中說的最小延遲

在第二段代碼中我們會看到,首先會在 延遲時間 和 1ms 之間取一個最大值,換句話說,在不滿足嵌套層級的情況下,最小延遲時間設(shè)置為 1ms,這也解釋了為什么在 chrome 中測試 setTimeout 是上面的結(jié)果。

在 chromium 的注釋中,解釋了為什么要設(shè)置 minimumInterval = 4ms。簡單來講,本身 chromium 團(tuán)隊想要設(shè)置更低的延遲時間(其實他們期望達(dá)到亞毫秒級別),但是由于某些網(wǎng)站對 setTimeout 這種計時器不良的使用,設(shè)置延遲過低會導(dǎo)致 CPU-spinning,因此 chromium 做了些測試,選定了 4ms 作為其 minimumInterval。

到這里為止,從瀏覽器廠商角度和 HTML standard 規(guī)范角度都解釋了 4ms 的來源和其更加精確的定義。

三、如何實現(xiàn)立刻執(zhí)行的定時器

假設(shè)我們就需要一個「立刻執(zhí)行」的定時器呢?有什么辦法繞過這個 4ms 的延遲嗎,其實可以采用 postMessage 來實現(xiàn)真正 0 延遲的定時器:

(function () {
  var timeouts = [];
  var messageName = 'message-zeroTimeout';
 
  // 保持 setTimeout 的形態(tài),只接受單個函數(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 對象上
  window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回調(diào)函數(shù)的執(zhí)行時機和 setTimeout 類似,都屬于宏任務(wù),所以可以簡單利用 postMessage 和 addEventListener('message') 的消息通知組合,來實現(xiàn)模擬定時器的功能,再利用上面的嵌套定時器的例子來跑一下測試:

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

1. 測試驗證

從理論上來說,由于 postMessage 的實現(xiàn)沒有被瀏覽器引擎限制速度,一定是比 setTimeout 要快的,這里用數(shù)據(jù)進(jìn)行驗證:分別用 postMessage 版定時器和傳統(tǒng)定時器做一個遞歸執(zhí)行計數(shù)函數(shù)的操作,看看同樣計數(shù)到 100 分別需要花多少時間

function runtest() {
  let output = document.getElementById('output');
  let outputText = document.createTextNode('');
  output.appendChild(outputText);
  function printOutput(line) {
    outputText.data += line + '\n';
  }
 
  let i = 0;
  let startTime = Date.now();
  // 通過遞歸 setZeroTimeout 達(dá)到 100 計數(shù)
  // 達(dá)到 100 后切換成 setTimeout 來實驗
  function test1() {
    if (++i == 100) {
      let 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);
 
  // 通過遞歸 setTimeout 達(dá)到 100 計數(shù)
  function test2() {
    if (++i == 100) {
      let endTime = Date.now();
      printOutput(
        '100 iterations of setTimeout(0) took ' +
          (endTime - startTime) +
          ' milliseconds.'
      );
    } else {
      setTimeout(test2, 0);
    }
  }
}

結(jié)論如下:

2. Performance 面板分析

我們打開 Performance 面板,看看更直觀的可視化界面中,兩個版本的定時器是如何分布的:

左邊的 postMessage 版本的定時器分布非常密集,大概在 5ms 以內(nèi)就執(zhí)行完了所有的計數(shù)任務(wù)

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

3. 無延遲的定時器的作用

你可能會有疑問,什么應(yīng)用場景下需要用到無延遲的定時器?其實在 React 的源碼中,做時間切片的部分就用到了:

const channel = new MessageChannel();
const port = channel.port2;
 
// 每次 port.postMessage() 調(diào)用就會添加一個宏任務(wù)
// 該宏任務(wù)為調(diào)用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;
 
const scheduler = {
  scheduleTask() {
    // 挑選一個任務(wù)并執(zhí)行
    const task = pickTask();
    const continuousTask = task();
 
    // 如果當(dāng)前任務(wù)未完成,則在下個宏任務(wù)繼續(xù)執(zhí)行
    if (continuousTask) {
      port.postMessage(null);
    }
  },
};

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

以上就是淺析JavaScript定時器setTimeout的時延問題的詳細(xì)內(nèi)容,更多關(guān)于JavaScript setTimeout時延的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論