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

淺析JavaScript定時(shí)器setTimeout的時(shí)延問題

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

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

一、為什么定時(shí)器有最小時(shí)延

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

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

MDN 文檔:在瀏覽器中,每調(diào)用一次定時(shí)器的最小間隔是 4ms,這通常是由于函數(shù)嵌套導(dǎo)致(嵌套層級(jí)達(dá)到一定深度),5 層以上的定時(shí)器嵌套會(huì)導(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í)行的時(shí)候延遲來到了 4ms 以上:

2. 非活動(dòng)標(biāo)簽的超時(shí)

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

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

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

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

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

4. 超時(shí)延遲

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

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

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

5. 在加載頁面時(shí)推遲超時(shí)

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

二、為什么定時(shí)器最小時(shí)延是4ms

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

1. HTML standard

在 HTML standard 8.6 Timers-2020/6/23 中對(duì)于 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
  • 如果嵌套的層級(jí)超過了 5 層,并且 timeout 小于 4ms,則設(shè)置 timeout 為 4ms

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

2. 瀏覽器源碼

除了尋找規(guī)范的出處之外,還可以去瀏覽器的源碼中尋找答案,我們進(jìn)入到谷歌瀏覽器源碼中來查找用來設(shè)置計(jì)時(shí)器延時(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è)置了幾個(gè)常量:

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

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

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

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

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

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

(function () {
  var timeouts = [];
  var messageName = 'message-zeroTimeout';
 
  // 保持 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 類似,都屬于宏任務(wù),所以可以簡(jiǎn)單利用 postMessage 和 addEventListener('message') 的消息通知組合,來實(shí)現(xiàn)模擬定時(shí)器的功能,再利用上面的嵌套定時(shí)器的例子來跑一下測(cè)試:

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

1. 測(cè)試驗(yàn)證

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

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 計(jì)數(shù)
  // 達(dá)到 100 后切換成 setTimeout 來實(shí)驗(yàn)
  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 計(jì)數(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 面板,看看更直觀的可視化界面中,兩個(gè)版本的定時(shí)器是如何分布的:

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

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

3. 無延遲的定時(shí)器的作用

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

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

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

相關(guān)文章

最新評(píng)論