自定义hook 之 切换后台进程冻结导致定时器停止的解决方法

402 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

PC 上的 FirefoxChromeSafari 等浏览器,都会自动把未激活页面中的 JavaScript 定时器(setTimeoutsetInterval)间隔最小值改为1秒以上。这是因为间隔很小的定时器一般用来做UI更新(例如用定时器实现的动画),让用户不可见的页面上的定时器跑慢一些,既节省资源又不会影响体验。对移动浏览器来说,内存,CPU,带宽等资源更加宝贵,设备移动的上浏览器往往会直接冻结所有未激活页面上的所有定时器。

document 上可以监听 visibilitychange 事件

visibilitychange 当其选项卡的内容变得可见或被隐藏时,会在文档上触发 visibilitychange (能见度更改)事件。

document.addEventListener("visibilitychange", function() {
  if (document.visibilityState === 'visible') {
    //	浏览器选项卡可见
  } else {
    //	浏览器选项卡不可见
  }
  
  if (!document.hidden) {
    //	浏览器选项卡可见
  } else {
    //	浏览器选项卡不可见
  }
});

我们的 hook 传入合法的时间戳,返回是否需要更新的 boolean 值。

import { useEffect, useRef, useState } from 'react';


/** 有效期,时间戳timestamp、或者合法时间字符串 */
type IExpires = number | string;

/**
 * 定时器 hook,tab 页可见时刷新定时器。用于解决切换后台进程冻结导致定时器停止。
 * @param {number | string} expires 定时器结束的时间,时间戳或者 dateString
 * @returns {boolean} 定时器是否结束
 */
function useTimer(expires: IExpires): boolean {
  const [ts, setTs] = useState<number>(getValidTime(expires)); // 定时器结束的时间戳
  const [isExpired, setIsExpired] = useState(false); // 是否结束
  const timer = useRef<number>(0);

  const setCountdown = (ts: number) => {
    const delay = Math.max(ts - Date.now(), 0);
    clearInterval(timer.current);
    if (delay === 0) {
      setIsExpired(true);
    } else {
      setIsExpired(false);
      timer.current = window.setTimeout(() => {
        setIsExpired(true);
      }, delay);
    }
  };

  useEffect(() => {
    setIsExpired(false);
    setTs(getValidTime(expires));
  }, [expires]);

  useEffect(() => {
    setCountdown(ts); // 时间更改时设置定时器
  }, [ts]);

  return isExpired;
}

export default useTimer;

/**
 * 判断时间格式是否合法,合法返回转换的时间戳。非法返回当前时间(定时器立即结束)
 */
function getValidTime(time: IExpires): number {
  const ts = new Date(time).getTime();
  return Number.isNaN(ts) ? Date.now() : ts;
}

用户打开或回到页面,刷新定时器(用于解决切换后台进程冻结导致定时器停止。)

选项卡能见度更改事件

useEffect(() => {
    function refreshCountdown() {
      if (document.visibilityState === 'visible') {
        setCountdown(ts);
      }
    }
    document.addEventListener('visibilitychange', refreshCountdown);
    return () => {
      document.removeEventListener('visibilitychange', refreshCountdown);
    };
  }, [ts]);