-
Notifications
You must be signed in to change notification settings - Fork 128
Description
问题
前端同学们依然会有很多时候会碰到需要使用定时器的场景,比如轮播动画、轮询数据等。很多时候大家都是随手写一个 setInterval 来完成需求。
比如下面的一个典型的计时器场景:
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return (
<div>{count}</div>
);
}眼见的小伙伴可能已经发现了几个问题:
- 没有写
clearInterval - 直接在闭包中取值
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
看似我们两行代码就把问题解决了,然而上面的代码其实依然存在一些隐患,很容易导致后续更新代码的时候出现问题:
- 因为闭包问题的存在(严格来说,是因为
setInterval中永远执行的是第一次render时的匿名函数,没有更新),如果在setInterval里面取外面的值,比如props,那么依然无法取到最新的值 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行代码中解决了几个关键问题:
- 通过
useRef保证引用最新的callback,完美解决了闭包带来的问题 - 自动执行
clearInterval - 自动处理
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,首先需要实现一个 setRafInterval 和 cancelRafInterval,用以替代 setInterval和 clearInterval
实现 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;
};需要注意两个地方:
- 对于没有RAF的环境,主要是
SSR渲染时,需要降级到setInterval - 由于多次调用了
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 模块外部,其实可以直接用上面的 setRafInterval和 cancelRafInterval 替换掉 setInterval和 clearInterval;
实现 useRafInterval
有了 setRafInterval 和 cancelRafInterval之后,可以着手来实现useRequestInterval了。这里我直接借鉴了ahooks中 useInterval的实现,做了一个简单的替换即可:
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
另外发现提交了这个之后,不知什么时候又多了 useRafTimeout 和 useRafState,形成了一个小小的 raf家族