防抖 与 内部状态 处理
现在,我们 看看 这一段 代码:
const sendRequest = useCallback((value: string) => {
console.log('Changed value:', value);
}, [])
一个 常见的 缓存函数 会 接受 value 作为 参数。这个 value 通过 放抖函数 的 input 组件 过来的。我们 在 onChange 的 回调 函数内,就 传递了 value 的值:
const onChange = (e) => {
const value = e.target.value;
setValue(value);
// value is coming from input change event directly
debouncedSendRequest(value);
}
但是我们在状态中也有这个值。我不能直接从那里使用它吗?也许我有一连串这样的回调,一遍又一遍地传递这个值真的很困难。也许我还想访问另一个状态变量。像这样通过回调传递它是没有意义的。或者也许我只是讨厌回调和参数,只想使用状态,仅仅是因为这样。应该很简单,不是吗?
当然,再一次,没有什么事情像看起来那么简单。如果我去掉参数并使用状态中的值,我就必须将其添加到 useEffect 钩子的依赖项中:
const Input = () => {
const [value, setValue] = useState('initial');
const sendRequest = useCallback(() => {
// value is now coming from state
console.log('Changed value:', value);
// adding it to dependencies
}, [value]);
};
正因为如此,sendRequest 函数会随着每次值的变化而改变。这就是记忆化(memoization)的工作原理。在重新渲染过程中,只要依赖项不改变,值就会保持不变。这意味着我们经过记忆化处理的防抖(debounce)调用现在也会不断改变:它将 sendRequest 作为依赖项,而现在 sendRequest 会随着每次状态更新而改变。
// this will now change on every state update
// because sendRequest has dependency on state
const debouncedSendRequest = useMemo(() => {
return debounce(sendRequest, 1000);
}, [sendRequest]);
代码示例: advanced-react.com/examples/11…
那么,有什么东西能够解决这个问题吗?当然,那就是Refs了。如果你搜索关于React和防抖的文章,有一半的文章会提及可以通过useRef避免因重新渲染产生新的防抖函数。
比如,这样:
const Input = () => {
// creating ref and initializing it with the debounced backend call
const ref = useRef(
debounce(() => {
// this is our old "debouncedSendRequest" function
}, 500),
);
const onChange = (e) => {
const value = e.target.value;
// calling the debounced function
ref.current();
};
};
这段代码看起来就比用useMemo和useCallback容易多了。
不幸的是,这段代码只对回调中没有状态的情况生效。还记得之前提及的过时闭包问题吗?一个Ref的初始值生成后,就不会更新了。它在页面被挂载或者被初始化时,就被“冻结”了。
我们知道,在Refs内使用函数,需要在useEffect钩子来更新函数。否则,这个闭包就过时了:
const Input = () => {
const [value, setValue] = useState();
// creating ref and initializing it with the debounced backend call
const ref = useRef(
debounce(() => {
// send request to the backend here
}, 500),
);
useEffect(() => {
// updating ref when state changes
ref.current = debounce(() => {
// send request to the backend here
}, 500);
}, [value]);
const onChange = (e) => {
const value = e.target.value;
// calling the debounced function
ref.current();
};
};
不幸的是,怎么整的话,其实和使用useCallback差不多,防抖函数会在每次value更新时重新生成,里面的timer也会重新生成,所以这个防抖函数本质上还是一个延时函数···
代码示例: advanced-react.com/examples/11…
而解决这个问题的方法,就是在useEffect中使用cleanup函数,并在重新复值前重制这个防抖函数。
useEffect(() => {
// updating ref when state changes
ref.current = debounce(() => {}, 500);
// cancel the debounce callback before
return () => ref.current.cancel();
}, [value]);
在这种情况下,每次更新时我们都会舍弃 “旧的” 防抖闭包,然后开启一个新的。这对于防抖来说是个不错的解决方案。但遗憾的是,它对节流并不适用。如果我一直取消节流函数,那么它永远没有机会在本该触发的时间间隔之后触发,而节流原本就应该在特定间隔触发。我想要一个更通用的解决方案。
这也是一个运用上一章详细探讨过的跳出闭包陷阱解决方案的绝佳场景!我们所要做的,就是把 sendRequest 赋值给一个 Ref 对象,在 useEffect 里更新这个 Ref,这样就能访问到最新的闭包,然后在闭包内部触发 ref.current。要记住:Ref 对象是可变的,而且闭包不会进行深度克隆。仅仅是对那个可变对象的引用被 “冻结” 了,我们仍然可以随时改变它所指向的对象。
这是代码:
const Input = () => {
const [value, setValue] = useState();
const sendRequest = () => {
// send request to the backend here
// value is coming from state
console.log(value);
}
// creating ref and intializing it with the sendRequest function
const ref = useRef(sendRequest);
useEffect(() => {
// updating ref when state change
// now, ref.current will have the latest sendRequest with access to the latest state
ref.current = sendRequest;
}, [value]);
// creating debounced callback only once - on mount
const debouncedCallback = useMemo(() => {
// func will be created only once - on mount
const func = () => {
// ref is mutable! ref.current is a reference to the latest sendRequest
ref.current?.();
}
// debounce the func that was created once, but has access to the latest sendRequest
return debounce(func, 1000);
// no dependencies ! never gets updated
}, [])
const onChange = (e) => {
const value = e.target.value;
// calling the debounced function
debouncedCallback();
}
}
之后,我们就可以把它抽象成一个钩子了:
const useDebounce = (callback) => {
const ref = useRef();
useEffect(() => {
ref.current = callback;
}, [callback]);
const debouncedCallback = useMemo(() => {
const func = () => {
ref.current?.();
}
return debounce(func, 1000);
}, [])
return debouncedCallback;
}
之后,我们就可以愉快地调用这个钩子了:
const Input = () => {
const [value, setValue] = useState();
const debounceRequest = useDebounce(() => {
// send request to the backend
// access to the lastest state here
console.log(value);
});
const onChange = (e) => {
const value = e.target.value;
setValue(value);
debounceRequest();
}
return <input onChange={onChange} value={value} />
}