Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React中如何更优雅的使用定时器 #51

Open
lihongxun945 opened this issue May 7, 2022 · 0 comments
Open

React中如何更优雅的使用定时器 #51

lihongxun945 opened this issue May 7, 2022 · 0 comments

Comments

@lihongxun945
Copy link
Owner

问题

前端同学们依然会有很多时候会碰到需要使用定时器的场景,比如轮播动画、轮询数据等。很多时候大家都是随手写一个 setInterval 来完成需求。
比如下面的一个典型的计时器场景:

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []);
  return (
    <div>{count}</div>
  );
}

眼见的小伙伴可能已经发现了几个问题:

  1. 没有写 clearInterval
  2. 直接在闭包中取值 count 会有问题

上面两个问题很好解决,我们稍微修改一下代码即可:

function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);
  return (
    <div>{count}</div>
  );
}

useInterval

看似我们两行代码就把问题解决了,然而上面的代码其实依然存在一些隐患,很容易导致后续更新代码的时候出现问题:

  1. 因为闭包问题的存在(严格来说,是因为setInterval中永远执行的是第一次 render 时的匿名函数,没有更新),如果在 setInterval 里面取外面的值,比如 props,那么依然无法取到最新的值
  2. setInterval 的参数不是“响应式的”,比如把1000 改成一个变量,那么要记得在 useEffect 中加一下依赖

关于setInterval 错误用法会导致的问题,以及推荐的正确用法,Dan有一篇较早的博客详细进行了探讨 https://overreacted.io/making-setinterval-declarative-with-react-hooks/

这篇文章中写了一个 useInterval,完整实现如下:

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

useInterval 的实现非常简单,19行代码中解决了几个关键问题:

  1. 通过 useRef 保证引用最新的callback,完美解决了闭包带来的问题
  2. 自动执行 clearInterval
  3. 自动处理 delay 变化

当然我们现在不用复制这段代码,可以直接用 ahooks 提供的 useInterval 即可 https://ahooks.js.org/zh-CN/hooks/use-interval

用 useInterval改造一下我们的代码:

function App() {
  const [count, setCount] = useState(0);
  useInterval(() => {
    setCount(count+1);
  }, 1000);
  return (
    <div>{count}</div>
  );
}

代码看起来干净整洁多了。

解决性能问题

最近实现一个通过轮询更新数据的功能时发现一个问题,这个系统是一个开发工具,几乎一直打开着,但是很多时候会切换到别的页面,在页面不显示的时候,其实没有必要更新,那么用 useInterval 就造成了很大性能浪费。
按这个思路,我用 requestAnimationFrame 模拟实现了 setInterval ,在浏览器每一次执行绘制任务前触发,如果页面没有显示在前台,那么就不会执行回调函数,完美解决了后台不需要刷新的问题。
这里我打算写一个 useRafInterval,在绝大多数场景下可以直接搜索替换掉 useInterval。为了实现 useRafInterval,首先需要实现一个 setRafIntervalcancelRafInterval,用以替代 setIntervalclearInterval

实现 setRafInterval 和 cancelRafInterval

setRafInterval基本思路是在 requestAnimationFrame中判断时间进行循环调用,代码比较简单直接上代码:

interface Handle {
  id: number | NodeJS.Timer;
}

const setRafInterval = function (callback: () => void, delay: number = 0): Handle {
  if (typeof requestAnimationFrame === typeof undefined) {
    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;
};

需要注意两个地方:

  1. 对于没有RAF的环境,主要是SSR渲染时,需要降级到 setInterval
  2. 由于多次调用了 requestAnimationFrame,所以id是会变化的,这样就没法直接返回id,需要用一个对象包装一下,目前没有找到更优雅的方式 =。=

cancelRafInterval 代码更简单一些:

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);
};

这里逻辑上很简单,需要注意的是对id类型的判断,由于typeof cancelAnimationFrame === typeof undefined不是对 handle本身的类型判断,TS无法根据这个条件推断出 handle.id的类型,需要增加一个类型守卫才可以。

React 模块外部,其实可以直接用上面的 setRafIntervalcancelRafInterval 替换掉 setIntervalclearInterval;

实现 useRafInterval

有了 setRafIntervalcancelRafInterval之后,可以着手来实现useRequestInterval了。这里我直接借鉴了ahooksuseInterval的实现,做了一个简单的替换即可:

function useRafInterval(
  fn: () => void,
  delay: number | undefined,
  options?: {
    immediate?: boolean;
  },
) {
  const immediate = options?.immediate;

  const fnRef = useLatest(fn);

  useEffect(() => {
    if (typeof delay !== 'number' || delay < 0) return;
    if (immediate) {
      fnRef.current();
    }
    const timer = setRafInterval(() => {
      fnRef.current();
    }, delay);
    return () => {
      clearRafInterval(timer);
    };
  }, [delay]);
}

想用的小伙伴不用复制上面的代码了,useRafInterval 已经在 ahooks中发布了,详细的使用文档可以参考Ahooks官方文档: https://ahooks.js.org/zh-CN/hooks/use-raf-interval
另外发现提交了这个之后,不知什么时候又多了 useRafTimeoutuseRafState,形成了一个小小的 raf家族

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant