在工作中使用到 setTimeout 定时器时,一般不需要去担心清除定时器。但如果在使用 React 开发时,去会出现一些讨厌的边界案例。
如果我们想在某段时间后操作数据,但在这个时间节点之前组件也许已经被卸载了,但是定时器设置的函数还在尝试运行。这也就触发了一些操作被还原的边界案例,也有可能在控制台中得到一些内存泄漏的信息。
清除定时器
一般的建议就是要跟踪在代码中创建的定时器并清理。在 React 中,组件的卸载需要涉及生命周期,对于类组件时 componentWillUnmount,而对于函数组件则是利用 useEffect,如果 useEffect 返回一个函数,React 将会在执行清除操作时调用它。
一个简单的案例代码如下:
export default function Test() {
const [show, setShow] = useState(false);
useEffect(() => {
const test = window.setTimeout(() => {
setShow(false);
}, 1500);
return () => {
clearInterval(test);
};
}, []);
return (
<div>
<h1>Loading...</h1>
{show && <p>I'm fully loaded now</p>}
</div>
);
}
使用 ref 来清除定时器也是不错的选择
const timeoutRef = useRef();
useEffect(() => {
timeoutRef.current = window.setTimeout(() => {
setShow(false);
}, 1500);
return () => clearInterval(timeoutRef.current);
}, []);
这样既可以实现在 React 组件卸载时清除定时器。不过每次都要想着清除或许有些麻烦,不妨创建一个 hook 来实现 React 中的定时器以及卸载时清除。
useTimeout
我们可以引入一个 useTimeout Hook,这个 Hook 应该有以下 options。
- 接收回调函数(超时后应该发生的动作)callback
- 接收延迟(超时的时间)delay
- 返回一个可以被调用的函数来启动它 return
import { useCallback, useEffect, useRef, useMemo } from 'react';
export default function useTimeout(callback, delay) {
const timeoutRef = useRef();
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
return () => window.clearTimeout(timeoutRef.current);
}, []);
const memoizedCallback = useCallback(
(args) => {
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = null;
callbackRef.current?.(args);
}, delay);
},
[delay, timeoutRef, callbackRef]
);
return useMemo(() => memoizedCallback, [memoizedCallback]);
}
-
首先,传递的参数是回调 callback 和延迟时间 delay。然后在 Hook 中添加两个 ref 来跟踪定时器和回调函数。
-
然后我们需要两个useEffects,一个用来监听回调,以防它在渲染后发生变化(回调内改变了任何状态时)。第二个是用来处理超时的清理效果(当组件被卸载时)。
-
然后创建一个 useCallback,在其中首先清除 ref 中的任何现有定时器,然后分配新定时器。这整个 useCallback 函数会监听所有变量的变化,保证不会过度渲染。
-
最后一部分是返回一个记忆化的函数,并监听其回调的变化。
这可能看起来有些矫枉过正,不过也是锻炼一下对于 setTiemout 以及 hook 的理解,让定时器清除更加彻底。