如何在React中预防过多的JS事件处理程序?

7

问题

一个应用程序需要窗口的内部尺寸。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 参考性能

调整大小性能,累积数百个监听器最大监听器:900+ 调整大小性能

注释掉window.addEventListener的调整大小性能最大监听器:49 没有addEventListener的调整大小性能

环境

  • 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项目中复制了此问题

询问

有人能解释这个问题吗?我被难住了! (相关:其他人能否复制此问题?)


1
不会解决问题,但是提示:将 useState<DOMRect>(getWindowRect()); 更改为 useState(getWindowRect);,以便在每次渲染时不调用 DOMRect。也可以在组件外部声明该函数,以避免在每次渲染时创建它。 - CertainPerformance
2
我不确定这个方法是否可行,但你可以尝试在主 hook 函数体中定义 resizeHandler,并使用 useCallback 进行记忆化。这样只会有一个事件监听器,其引用将被保存。编辑:我假定你已经验证该 effect 只运行一次。 - Jayce444
尝试将 const resizeHandler = () => { setRect(getWindowRect()); }; 放在 useEffect() 内部。编辑 啊,我没有注意到这是你最初问题中的情况。根据React文档,它应该放在内部。 - Patrick Roberts
1
我认为问题与你的代码无关,看起来事件处理程序是由react-dom.development.js中的invokeGuardedCallbackDev注册的。它还看起来像是在足够长时间后被清理 - Patrick Roberts
1
嗯嗯嗯...我知道那里会有一些聪明的人能够帮我解决问题。刚在生产模式下进行了测试,事实上这确实消除了那些症状。非常感谢PatrickRoberts和Aleksey L. - oclyke
显示剩余5条评论
1个回答

0
正如Patrick Roberts和Aleksey L.在评论中指出的那样,问题实际上并不是问题,因为:

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接