首頁 > 軟體

定時器在頁面最小化時不執行實現範例

2022-07-12 22:06:58

引言

本文是深入淺出 ahooks 原始碼系列文章的第七篇,這個系列的目標主要有以下幾點:

  • 加深對 React hooks 的理解。
  • 學習如何抽象自定義 hooks。構建屬於自己的 React hooks 工具庫。
  • 培養閱讀學習原始碼的習慣,工具庫是一個對原始碼閱讀不錯的選擇。

注:本系列對 ahooks 的原始碼解析是基於 v3.3.13。自己 folk 了一份原始碼,主要是對原始碼做了一些解讀,可見 詳情

今天我們來聊聊定時器。

useInterval 和 useTimeout

看名稱,我們就能大概知道,它們的功能對應的是 setInterval 和 setTimeout,那對比後者有什麼優勢?

先看 useInterval,程式碼簡單,如下所示:

function useInterval(
  fn: () => void,
  delay: number | undefined,
  options?: {
    immediate?: boolean;
  },
) {
  const immediate = options?.immediate;
  const fnRef = useLatest(fn);
  useEffect(() => {
    // 忽略部分程式碼...
    // 立即執行
    if (immediate) {
      fnRef.current();
    }
    const timer = setInterval(() => {
      fnRef.current();
    }, delay);
    // 清除定時器
    return () => {
      clearInterval(timer);
    };
    // 動態修改 delay 以實現定時器間隔變化與暫停。
  }, [delay]);
}

跟 setInterval 的區別如下:

  • 可以支援第三個引數,通過 immediate 能夠立即執行我們的定時器。
  • 在變更 delay 的時候,會自動清除舊的定時器,並同時啟動新的定時器。
  • 通過 useEffect 的返回清除機制,開發者不需要關注清除定時器的邏輯,避免記憶體洩露問題。這點是很多開發者會忽略的點。

useTimeout 跟上面很類似,如下所示,不再做額外解釋:

function useTimeout(fn: () => void, delay: number | undefined): void {
  const fnRef = useLatest(fn);
  useEffect(() => {
    // ...忽略部分程式碼
    const timer = setTimeout(() => {
      fnRef.current();
    }, delay);
    return () => {
      clearTimeout(timer);
    };
  // 動態修改 delay 以實現定時器間隔變化與暫停。
  }, [delay]);
}

setTimeout 和 setInterval 的問題

首先,setTimeout 和 setInterval 作為事件迴圈中宏任務的“兩大主力”,它的執行時機不能跟我們預期一樣準確的,它需要等待前面任務的執行。比如下面的 setTimeout 的第二個引數設定為 0,並不會立即執行。

setTimeout(() => {
  console.log('test');
}, 0)

另外還有一種情況,setTimeout 和 setInterval 在瀏覽器不可見的時候(比如最小化的時候),不同的瀏覽器中設定不同的時間間隔的時候,其表現不一樣。根據 當瀏覽器切換到其他分頁或者最小化時,你的js定時器還準時嗎? 這篇文章的實踐結論如下:

谷歌瀏覽器中,當頁面處於不可見狀態時,setInterval 的最小間隔時間會被限制為 1s。火狐瀏覽器的 setInterval 和谷歌特性一致,但是 ie 瀏覽器沒有對不可見狀態時的 setInterval 進行效能優化,不可見前後間隔時間不變。

在谷歌瀏覽器中,setTimeout在瀏覽器不可見狀態下間隔低於1s的會變為1s,大於等於1s的會變成N+1s的間隔值。火狐瀏覽器下setTimeout的最小間隔時間會變為1s,大於等於1s的間隔不變。ie瀏覽器在不可見狀態前後的間隔時間不變。

這個結論,我沒有驗證過,但看起來差異挺大,其中還提到了另外一個選擇,就是 requestAnimationFrame。

window.requestAnimationFrame() 告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前呼叫指定的回撥函數更新動畫。該方法需要傳入一個回撥函數作為引數,該回撥函數會在瀏覽器下一次重繪之前執行

為了提高效能和電池壽命,因此在大多數瀏覽器裡,當requestAnimationFrame() 執行在後臺分頁或者隱藏的 <iframe> 裡時,requestAnimationFrame() 會被暫停呼叫以提升效能和電池壽命

所以,ahooks 也提供了使用 requestAnimationFrame 進行模擬定時器處理的 hook,我們一起來看下。

useRafInterval 和 useRafTimeout

直接看 useRafInterval。(useRafTimeout 和 useRafInterval 類似,這裡不展開細說)。

function useRafInterval(
  fn: () => void,
  delay: number | undefined,
  options?: {
    immediate?: boolean;
  },
) {
  const immediate = options?.immediate;
  const fnRef = useLatest(fn);
  useEffect(() => {
    // 省略部分程式碼...
    const timer = setRafInterval(() => {
      fnRef.current();
    }, delay);
    return () => {
      clearRafInterval(timer);
    };
  }, [delay]);
}

可以看到,跟前面的 useInterval 大部分程式碼邏輯都是一樣的,只是定時使用了 setRafInterval 方法,清除定時器用了 clearRafInterval

setRafInterval

直接上程式碼:

const setRafInterval = function (callback: () => void, delay: number = 0): Handle {
  if (typeof requestAnimationFrame === typeof undefined) {
    // 如果不支援,還是使用 setInterval
    return {
      id: setInterval(callback, delay),
    };
  }
  // 開始時間
  let start = new Date().getTime();
  const handle: Handle = {
    id: 0,
  };
  const loop = () => {
    const current = new Date().getTime();
    // 當前時間 - 開始時間,大於設定的間隔,則執行,並重置開始時間
    if (current - start >= delay) {
      callback();
      start = new Date().getTime();
    }
    handle.id = requestAnimationFrame(loop);
  };
  handle.id = requestAnimationFrame(loop);
  return handle;
};

首先是用 typeof 判斷進行相容邏輯處理,假如不相容,則兜底使用 setInterval。

初始記錄一個 start 的時間。

在 requestAnimationFrame 回撥中,判斷現在的時間減去開始時間有沒有達到間隔,假如達到則執行我們的 callback 函數。更新開始時間。

clearRafInterval

清除定時器。

function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
  return typeof cancelAnimationFrame === typeof undefined;
}
// 清除定時器
const clearRafInterval = function (handle: Handle) {
  if (cancelAnimationFrameIsNotDefined(handle.id)) {
    return clearInterval(handle.id);
  }
  cancelAnimationFrame(handle.id);
};

假如不支援 cancelAnimationFrame API,則通過 clearInterval 清除,支援則直接使用 cancelAnimationFrame 清除。

思考與總結

關於定時器,我們平時用得不少,但經常有同學容易忘記清除定時器,結合 useEffect 返回清除副作用函數這個特性,我們可以將這類邏輯一起封裝到 hook 中,讓開發者使用更加方便。

另外,假如希望在頁面不可見的時候,不執行定時器,可以選擇 useRafInterval 和 useRafTimeout,其內部是使用 requestAnimationFrame 進行實現。

以上就是定時器在頁面最小化時不執行實現範例的詳細內容,更多關於定時器頁面最小化不執行的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com