React/Redux、useEffect/useSelector、过时的闭包和事件侦听器
React/Redux, useEffect/useSelector, stale closures and event listeners
我有一个 React/Redux class 组件,我正在将其转换为功能组件。
以前,它有一个 componentDidMount
回调,它为从应用中其他地方调度的自定义事件添加了一个事件侦听器。这将被 useEffect
取代。事件侦听器在触发时调用此组件中其他位置的方法。
此方法执行的操作取决于从选择器中检索到的值。最初我传递 useEffect
一个空的依赖项数组,所以它只会在挂载时添加事件侦听器。然而,显然这会导致围绕选择器值的陈旧闭包。一个可行的解决方案是将选择器作为依赖项传递给它——但是,这会导致每次选择器的值发生变化时监听器都是 removed/re-added,这不是很好。
我正在考虑一种解决方案,它只添加一次事件侦听器,同时允许被调用的方法访问选择器的当前值。
示例代码:
const currentValue = useSelector(state => getValue(state));
useEffect(() => {
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
const handleEvent = () => {
console.log(currentValue)
}
照原样,这会在 currentValue
周围创建一个陈旧的闭包,因此在事件触发时,记录的值不一定是最新的。
在 useEffect
中将 []
更改为 [currentValue]
会产生预期的行为,但 removes/re-adds 每次更改 currentValue
时都会监听事件。
由于这不是组件的状态值,因此无法使用 console.log(currentValue => console.log(currentValue))
之类的回调来访问最新值。我也尝试过使用 useRef
来保留值,但我相信每次选择器的值发生变化时我都需要一些方法来更新 ref 值,这不是一个很好的解决方案。
在实际组件中,currentValue
的值在Redux中被其他组件修改,因此将其更改为状态值也不太可行。
我在想:
- 是否有解决方案可以在被调用的方法中刷新选择器的值;
- 是否唯一的解决方案是 remove/re-add 依赖项更改的侦听器,或者;
- 是否最好将此组件保留为 class 组件并完全绕过此问题(
componentDidMount
不会遇到陈旧的关闭问题。)
useRef
方法通常是解决此问题的方法,但您需要另一个 useEffect
来使用 currentValue
:
更新 ref
const currentValue = useSelector(state => getValue(state));
const valueRef = useRef();
useEffect(() => { valueRef.current = currentValue; }, [currentValue]);
useEffect(() => {
const handleEvent = () => {
console.log(valueRef.current)
}
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
但是,您也可以从 useEffect
中提取整个函数,并在挂钩中使用它,这样您就可以轻松地为事件处理创建自定义挂钩:
const useEventHandler = (eventName, eventHandler, eventTarget = document) => {
const eventHandlerRef = useRef();
useEffect(() => {
eventHandlerRef.current = eventHandler;
});
useEffect(() => {
const handleEvent = (...args) => eventHandlerRef?.current(...args);
eventTarget.addEventListener(eventName, handleEvent);
return (() => eventTarget.removeEventListener(eventName, handleEvent));
}, []);
}
使用钩子:
const currentValue = useSelector(state => getValue(state));
useEventHandler('my.custom.event', () => console.log(currentValue))
however, this results in the listener being removed/re-added every time the value of the selector changes, which isn't great.
为什么会出现问题?它会以任何可衡量的方式影响您的应用程序的性能吗?从不变性的角度来看,如果值发生变化,则事件侦听器会做一些不同的事情,因此将事件侦听器简单地替换为不同的侦听器是有意义的。
如果该值每秒更改多次(例如在每一帧或每次鼠标移动时),那么它确实会对性能造成小的影响,在这种情况下,Ori Drori 的解决方案可能是一个不错的解决方案,但它使代码更难理解,性能提升很小。
但如果该值每秒只更改几次或更少,我根本不会担心添加和删除事件侦听器。
我有一个 React/Redux class 组件,我正在将其转换为功能组件。
以前,它有一个 componentDidMount
回调,它为从应用中其他地方调度的自定义事件添加了一个事件侦听器。这将被 useEffect
取代。事件侦听器在触发时调用此组件中其他位置的方法。
此方法执行的操作取决于从选择器中检索到的值。最初我传递 useEffect
一个空的依赖项数组,所以它只会在挂载时添加事件侦听器。然而,显然这会导致围绕选择器值的陈旧闭包。一个可行的解决方案是将选择器作为依赖项传递给它——但是,这会导致每次选择器的值发生变化时监听器都是 removed/re-added,这不是很好。
我正在考虑一种解决方案,它只添加一次事件侦听器,同时允许被调用的方法访问选择器的当前值。
示例代码:
const currentValue = useSelector(state => getValue(state));
useEffect(() => {
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
const handleEvent = () => {
console.log(currentValue)
}
照原样,这会在 currentValue
周围创建一个陈旧的闭包,因此在事件触发时,记录的值不一定是最新的。
在 useEffect
中将 []
更改为 [currentValue]
会产生预期的行为,但 removes/re-adds 每次更改 currentValue
时都会监听事件。
由于这不是组件的状态值,因此无法使用 console.log(currentValue => console.log(currentValue))
之类的回调来访问最新值。我也尝试过使用 useRef
来保留值,但我相信每次选择器的值发生变化时我都需要一些方法来更新 ref 值,这不是一个很好的解决方案。
在实际组件中,currentValue
的值在Redux中被其他组件修改,因此将其更改为状态值也不太可行。
我在想:
- 是否有解决方案可以在被调用的方法中刷新选择器的值;
- 是否唯一的解决方案是 remove/re-add 依赖项更改的侦听器,或者;
- 是否最好将此组件保留为 class 组件并完全绕过此问题(
componentDidMount
不会遇到陈旧的关闭问题。)
useRef
方法通常是解决此问题的方法,但您需要另一个 useEffect
来使用 currentValue
:
const currentValue = useSelector(state => getValue(state));
const valueRef = useRef();
useEffect(() => { valueRef.current = currentValue; }, [currentValue]);
useEffect(() => {
const handleEvent = () => {
console.log(valueRef.current)
}
document.addEventListener('my.custom.event', handleEvent);
return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);
但是,您也可以从 useEffect
中提取整个函数,并在挂钩中使用它,这样您就可以轻松地为事件处理创建自定义挂钩:
const useEventHandler = (eventName, eventHandler, eventTarget = document) => {
const eventHandlerRef = useRef();
useEffect(() => {
eventHandlerRef.current = eventHandler;
});
useEffect(() => {
const handleEvent = (...args) => eventHandlerRef?.current(...args);
eventTarget.addEventListener(eventName, handleEvent);
return (() => eventTarget.removeEventListener(eventName, handleEvent));
}, []);
}
使用钩子:
const currentValue = useSelector(state => getValue(state));
useEventHandler('my.custom.event', () => console.log(currentValue))
however, this results in the listener being removed/re-added every time the value of the selector changes, which isn't great.
为什么会出现问题?它会以任何可衡量的方式影响您的应用程序的性能吗?从不变性的角度来看,如果值发生变化,则事件侦听器会做一些不同的事情,因此将事件侦听器简单地替换为不同的侦听器是有意义的。
如果该值每秒更改多次(例如在每一帧或每次鼠标移动时),那么它确实会对性能造成小的影响,在这种情况下,Ori Drori 的解决方案可能是一个不错的解决方案,但它使代码更难理解,性能提升很小。
但如果该值每秒只更改几次或更少,我根本不会担心添加和删除事件侦听器。