问题
_.debounce 创建一个函数,用于消除对同一函数实例的连续调用。但这会在每次渲染时创建一个新的,因此连续调用不会调用同一个实例。
您需要跨渲染重用相同的功能。有一种使用钩子的简单方法......还有一种更好的方法:
简单的(有缺陷的)解决方案 - useCallback
在渲染中保存内容最直接的方法是useMemo/useCallback。 (useCallback 实际上只是 useMemo 对于函数的一个特例)
const debouncedCallback = useCallback(_.debounce(
(field, payload) => handleChangeAt(field, payload),
500,
), [handleChangeAt])
我们遇到了handleChangeAt 的问题:它依赖于道具并在每次渲染时创建一个不同的实例,以便捕获最新版本的道具。如果我们只创建一次debouncedCallback,我们将捕获handleChangeAt 的第一个实例,并捕获props 的初始值,稍后会为我们提供陈旧的数据。
我们通过添加[handleChangeAt] 来解决此问题,以便在handleChangeAt 更改时重新创建debouncedCallback。然而,正如所写,handleChangeAt 会更改每个 渲染。因此,仅此更改不会改变初始行为:我们仍然会在每次渲染时重新创建 debouncedCallback。所以你也需要记住handleChangeAt:
const { handleFormChangeAt } = props;
const handleChangeAt = useCallback((field, payload) => {
handleFormChangeAt(/*...*/)
}, [handleFormChangeAt]);
(如果你不熟悉这种记忆,我强烈推荐 Dan Abramov 的 Complete Guide to useEffect,尽管我们实际上并没有在这里使用 useEffect)
这会将问题推到树上,您需要确保提供props.handleFormChangeAt 的任何组件也正在记忆它。但是,否则这个解决方案很大程度上可以工作......
更好的解决方案 - useRef
上一个解决方案的两个问题:如前所述,它将记忆问题推到了树上(特别是因为您依赖于作为道具传递的函数),但这样做的重点是我们可以重新创建在我们需要的时候运行,以避免过时的数据。
但是为了避免陈旧数据而重新创建将导致函数被重新创建,这将导致去抖动重置:所以之前解决方案的结果是通常去抖动,但如果 props 或 state 发生了变化,可能不会。
更好的解决方案要求我们实际上只创建一次记忆函数,但这样做的方式是避免过时的数据。我们可以通过使用 ref 来做到这一点:
const debouncedFunctionRef = useRef()
debouncedFunctionRef.current = (field, payload) => handleChangeAt(field, payload);
const debouncedChange = useCallback(_.debounce(
(...args) => debouncedFunctionRef.current(...args),
500,
), []);
这会将要反跳的函数的当前实例存储在 ref 中,并在每次渲染时更新它(防止过时数据)。但是,我们不是直接去抖动该函数,而是去抖动一个包装函数,该函数从 ref 读取当前版本并调用它。
由于回调唯一依赖的是一个 ref(它是一个常量、可变对象),所以 useCallback 可以将 [] 作为它的依赖项,因此我们每次只对函数进行一次反跳组件,正如预期的那样。
作为自定义钩子
这种方法可以移动到它自己的自定义钩子中:
const useDebouncedCallback = (callback, delay) => {
const callbackRef = useRef()
callbackRef.current = callback;
return useCallback(_.debounce(
(...args) => callbackRef.current(...args),
delay,
), []);
};
const { useCallback, useState, useRef, useEffect } = React;
const useDebouncedCallback = (callback, delay, opts) => {
const callbackRef = useRef()
callbackRef.current = callback;
return useCallback(_.debounce(
(...args) => callbackRef.current(...args),
delay,
opts
), []);
};
function Reporter({count}) {
const [msg, setMsg] = useState("Click to record the count")
const onClick = useDebouncedCallback(() => {
setMsg(`The count was ${count} when you clicked`);
}, 2000, {leading: true, trailing: false})
return <div>
<div><button onClick={onClick}>Click</button></div>
{msg}
</div>
}
function Parent() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => setCount(x => x+1), 500)
}, [])
return (
<div>
<div>The count is {count}</div>
<Reporter count={count} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
<div id="root" />