useLayoutEffect中addEventListener重复绑定事件如何仅注册一次?
问题根源
你的useLayoutEffect依赖数组包含isActive,每次isActive更新都会触发effect重新执行,且没有在effect清理阶段移除之前绑定的事件监听器,每次执行都会新增一组监听器,最终出现调用次数指数级增长、操作卡顿的问题。同时你直接给window.onresize赋值也会存在覆盖原有监听、无法清理的问题。
修复后的代码
import { useRef } from 'react'; // 组件内逻辑 const isActiveRef = useRef(isActive); // 同步isActive最新值 useEffect(() => { isActiveRef.current = isActive; }, [isActive]); // 滑动相关变量存在ref里,不会因为effect重跑重置 const touchStateRef = useRef({ touchStartX: 0, touchEndX: 0, movingTestimonials: false }); useLayoutEffect(() => { // 处理resize逻辑 const handleResize = () => { setWindowWidth(window.innerWidth); }; if (window) { setWindowWidth(window.innerWidth); window.addEventListener('resize', handleResize); } const handleGesture = () => { const { movingTestimonials, touchStartX, touchEndX } = touchStateRef.current; if (movingTestimonials) { return false; } touchStateRef.current.movingTestimonials = true; const diff = touchStartX - touchEndX; const moves = diff > 0 ? 1 : -1; // 从ref拿最新的isActive,不需要放到effect依赖 const next = (isActiveRef.current + moves) % Math.ceil(data.testimonials.length / testimonialsToShow()); const finalNext = next >= 0 ? next : testimonialsToShow() + 1 - next; setIsActive(finalNext); touchStateRef.current.movingTestimonials = false; }; const touchStartHandler = (e) => { touchStateRef.current.touchStartX = e.touches[0]?.pageX; }; const touchEndHandler = (e) => { touchStateRef.current.touchEndX = e.changedTouches[0]?.pageX; if (touchStateRef.current.touchEndX) { handleGesture(); } }; const targetNode = testimonialRef?.current; if (targetNode) { targetNode.addEventListener('touchstart', touchStartHandler, true); targetNode.addEventListener('touchend', touchEndHandler, true); console.log('added event listeners'); } // 核心:返回清理函数,移除所有绑定的监听器 return () => { if (window) { window.removeEventListener('resize', handleResize); } if (targetNode) { targetNode.removeEventListener('touchstart', touchStartHandler, true); targetNode.removeEventListener('touchend', touchEndHandler, true); } }; // 依赖数组为空,只会在组件挂载时执行一次 }, []);
修改说明
- 用
isActiveRef实时同步isActive的最新值,不需要把isActive放到useLayoutEffect的依赖数组中,避免effect重复执行 - 滑动相关的临时变量统一存到
touchStateRef中,不会因为effect重跑被重置 - 把
window.onresize改为addEventListener绑定,方便后续清理 - 新增effect清理函数,组件卸载/effect重跑时先移除所有之前绑定的事件监听器,避免重复绑定
- 最终
useLayoutEffect依赖数组设为空,只会在组件挂载时执行一次,事件监听器只会绑定一次
内容的提问来源于stack exchange,提问作者Toni Michel Caubet
相关产品推荐
相关产品推荐

