React函数组件热重载依赖未变时useEffect仍重复执行问题
核心原因
这个现象和React的依赖数组比对逻辑没有任何关系,是开发环境热模块替换(HMR,即热重载)的设计行为,不是代码bug。
热重载时构建工具会尽可能保留已有的运行时状态——这也是你观察到scene、camera的uuid始终不变、ref引用没有丢失的原因,但为了让你修改的代码逻辑及时生效,框架会重新执行组件模块代码、重跑所有effects,这个机制和依赖是否变化无关,哪怕你把依赖数组写成空数组、常量值,热重载时effect依然会触发。该行为仅存在于开发环境,生产环境构建后不存在这个问题。
解决方案
根据你的场景选以下方案即可,都是React标准写法,不会影响生产环境逻辑:
方案1:加effect清理逻辑(推荐,不影响热更新生效)
useEffect支持返回清理函数,热重载触发effect重跑前、组件卸载时都会先执行上一次effect的清理逻辑,在这里移除之前加载的模型、释放占用资源,就不会出现重复对象的问题:
useEffect( () => { console.log("CustomLog: Setting up scene"); camera.position.z = 5.0; let loadedModel: Three.Object3D | null = null; const gltfLoader = new GLTFLoader(); gltfLoader.load( "/Shoe.glb", (shoeGLB) => { loadedModel = shoeGLB.scene; scene.add(shoeGLB.scene); } ); // 清理逻辑:移除已加载模型、释放内存 return () => { if (!loadedModel) return; scene.remove(loadedModel); loadedModel.traverse((obj) => { const mesh = obj as Three.Mesh; if (mesh.isMesh) { mesh.geometry.dispose(); const material = mesh.material; Array.isArray(material) ? material.forEach(m => m.dispose()) : material.dispose(); } }) } }, [scene, camera] );
同时你需要修复动画循环的内存泄漏问题,现有代码每次热重载都会新增一个动画循环,长时间调试会导致页面卡顿:
// 组件内新增ref存动画id const animateIdRef = useRef<number>(0); const onContainerReferenced = useCallback( (container: HTMLDivElement) => { console.log(`CustomLog: Container element referenced`); if(container == null) { // 容器卸载时取消动画 if (animateIdRef.current) cancelAnimationFrame(animateIdRef.current); return; } if(orbitControlsRef.current === undefined) orbitControlsRef.current = new OrbitControls(camera, container); const orbitControls: OrbitControls = orbitControlsRef.current; (camera as any).aspect = container.clientWidth / container.clientHeight; renderer.setSize(container.clientWidth, container.clientHeight); container.appendChild(renderer.domElement); orbitControls.domElement = container; orbitControls.update(); function animate() { animateIdRef.current = requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); }, [] );
方案2:加初始化标记(适合大模型等重初始化场景)
如果不想每次热重载都重新加载资源,可以用ref存初始化标记,已经执行过初始化就直接跳过逻辑:
// 组件内新增初始化标记ref const isSceneInit = useRef(false); useEffect( () => { if (isSceneInit.current) return; console.log("CustomLog: Setting up scene"); camera.position.z = 5.0; const gltfLoader = new GLTFLoader(); gltfLoader.load( "/Shoe.glb", (shoeGLB) => { scene.add(shoeGLB.scene); } ); isSceneInit.current = true; }, [scene, camera] );
注意:这个方案下如果修改了场景初始化相关代码,热重载不会同步变更,需要手动刷新页面才能看到效果。
额外提示
不要为了规避热重载行为去修改依赖数组、用全局变量存实例等黑科技绕过React生命周期,上述方案都是官方推荐的标准副作用处理方式,生产环境下HMR逻辑不会执行,effect只会在组件挂载时跑一次,完全符合你只初始化一次的需求。
内容的提问来源于stack exchange,提问作者Yamin Nather
相关产品推荐
相关产品推荐

