You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 22:39:05