问题
一个应用程序需要窗口的内部尺寸。React 模式建议在一次性效果钩子中注册事件侦听器。调用window.addEventListener
只会发生一次,但事件侦听器会积累并对性能产生负面影响。
代码
这是重现此问题的简化源代码:
import React, {useState, useEffect} from 'react';
const getWindowRect = () => {
return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
}
// custom hook to track the window dimensions
const useWindowRect = () => {
// use state for the aspect ratio
let [rect, setRect] = useState(getWindowRect);
// useEffect w/o deps should only be called once
useEffect(() => {
const resizeHandler = () => { setRect(getWindowRect()); };
window.addEventListener('resize', resizeHandler);
console.log('added resize listener');
// return the cleanup function
return () => {
window.removeEventListener('resize', resizeHandler);
console.log('removed resize listener');
}
}, []);
// return the up-to-date window rect
return rect;
}
const App = () => {
const window_rect = useWindowRect();
return <div>
{window_rect.width/window_rect.height}
</div>
};
export default App;
测试
相关控制台输出如下:
added resize listener
当监听器只被添加一次时,无论应用程序重新渲染多少次,此处给出了预期结果。
参考,窗口大小未调整最大监听器:56
注释掉window.addEventListener
的调整大小性能最大监听器:49
环境
- React 16.13.1
- TypeScript 4.0.3
- WebPack 4.44.2
- Babel Loader 8.1.0
- Chrome 86.0.4240.111 (64位官方版本)
演示
假设在JSFiddle或CodePen上运行性能指标会比较困难,因此我在此仓库提供了完整的演示:oclyke-exploration/resize-handler-performance只要你安装了node和yarn,就可以轻松运行演示。
一般讨论
- 这种方法以前已经使用过,没有出现这些症状,但环境略有不同,没有包括TypeScript(这可能是由于交叉编译引起的吗?)
- 我简要地研究了一下,提供给
window.removeEventListener
的函数引用是否与提供给window.addEventListener
的函数引用相同-虽然当效果仅发生一次时,这甚至不应该发挥作用 - 有许多可能的方法来解决这个问题--这个问题旨在询问为什么这种预期能够工作的方法却不能工作
- 使用react-scripts 4.0.0在一个新的
create-react-app
项目中复制了此问题
询问
有人能解释这个问题吗?我被难住了! (相关:其他人能否复制此问题?)
useState<DOMRect>(getWindowRect());
更改为useState(getWindowRect);
,以便在每次渲染时不调用DOMRect
。也可以在组件外部声明该函数,以避免在每次渲染时创建它。 - CertainPerformanceresizeHandler
,并使用useCallback
进行记忆化。这样只会有一个事件监听器,其引用将被保存。编辑:我假定你已经验证该 effect 只运行一次。 - Jayce444const resizeHandler = () => { setRect(getWindowRect()); };
放在useEffect()
内部。编辑 啊,我没有注意到这是你最初问题中的情况。根据React文档,它应该放在内部。 - Patrick Robertsreact-dom.development.js
中的invokeGuardedCallbackDev
注册的。它还看起来像是在足够长时间后被清理。 - Patrick Roberts