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

React中useEffect为何在不符合条件时多次触发?

问题分析

你遇到的核心问题是每个Terrain组件都独立给document绑定了click和mouseover事件,12个组件就会生成12组重复的全局监听。每次点击或鼠标移动时,所有组件的事件处理器都会被触发,这就是日志中大量输出的原因——并非useEffect异常触发,而是重复绑定的监听导致冗余执行。虽然当前功能正常,但随着地形数量增加,会带来不必要的性能消耗,长期可能引发卡顿问题。

你尝试将onClick移到组件的div上,虽然解决了重复触发问题,但丢失了「点击组件外部关闭详情」的交互,因为此时不再监听全局点击事件。

解决方案

推荐将全局事件监听逻辑移到父组件统一处理,通过父组件维护全局状态(当前选中/hover的地形ID),子组件仅根据状态渲染对应内容。这种方式只需要2个全局监听,彻底解决重复绑定问题,同时保留所有交互功能。

父组件(地图容器)

import { useState, useEffect } from "react";
import Terrain from "./Terrain";

export default function Map() {
  // 维护全局选中/hover的地形ID
  const [selectedTerrainId, setSelectedTerrainId] = useState(null);
  const [hoveredTerrainId, setHoveredTerrainId] = useState(null);

  useEffect(() => {
    // 全局点击事件:判断是否点击在地形外部,是则清空选中状态
    const handleDocumentClick = (e) => {
      const allTerrains = Array.from(document.querySelectorAll('.terrain'));
      const isOutside = !allTerrains.some(terrain => terrain.contains(e.target));
      if (isOutside) setSelectedTerrainId(null);
    };

    // 全局鼠标悬停事件:判断当前hover的地形
    const handleDocumentMouseOver = (e) => {
      const hoveredTerrain = Array.from(document.querySelectorAll('.terrain'))
        .find(terrain => terrain.contains(e.target));
      setHoveredTerrainId(hoveredTerrain ? hoveredTerrain.id : null);
    };

    document.addEventListener('click', handleDocumentClick);
    document.addEventListener('mouseover', handleDocumentMouseOver);

    // 组件卸载时移除监听
    return () => {
      document.removeEventListener('click', handleDocumentClick);
      document.removeEventListener('mouseover', handleDocumentMouseOver);
    };
  }, []);

  // 示例地形数据(替换为你实际的数据源)
  const terrains = [
    { id: "terrain-1", src: "/path/to/img1.png", dataBrief: "地形1简介", dataDetailed: "地形1详细信息" },
    { id: "terrain-2", src: "/path/to/img2.png", dataBrief: "地形2简介", dataDetailed: "地形2详细信息" },
    // 剩余10个地形...
  ];

  return (
    <div className="map-container">
      {terrains.map(terrain => (
        <Terrain
          key={terrain.id}
          id={terrain.id}
          className={`terrain ${terrain.className}`}
          src={terrain.src}
          dataBrief={terrain.dataBrief}
          dataDetailed={terrain.dataDetailed}
          isClicked={selectedTerrainId === terrain.id}
          isHovered={hoveredTerrainId === terrain.id}
          onClick={() => {
            // 点击地形时切换选中状态
            setSelectedTerrainId(prev => prev === terrain.id ? null : terrain.id);
          }}
        />
      ))}
    </div>
  );
}

子组件(Terrain)

简化子组件逻辑,仅负责渲染和接收父组件传递的状态与事件:

import BriefTerrainInfo from "./BriefTerrainInfo";
import DetailedTerrainInfo from "./DetailedTerrainInfo";

export default function Terrain({ 
  src, className, id, dataDetailed, dataBrief, 
  isClicked, isHovered, onClick 
}) {
  return (
    <div className={className} id={id} onClick={onClick}>
      <img src={src} alt={`Terrain ${id}`} />
      {isHovered && <BriefTerrainInfo dataBrief={dataBrief} />}
      {isClicked && <DetailedTerrainInfo dataDetailed={dataDetailed} />}
    </div>
  );
}
替代方案(不修改父组件)

如果不想调整父组件结构,可以在子组件中用useCallback缓存事件处理器,确保每个组件仅绑定一次全局监听(但仍会存在12个监听,性能不如父组件统一处理):

import { useState, useRef, useEffect, useCallback } from "react";
import BriefTerrainInfo from "./BriefTerrainInfo";
import DetailedTerrainInfo from "./DetailedTerrainInfo";

export default function Terrain({ src, className, id, dataDetailed, dataBrief }) {
  const [isClicked, setIsClicked] = useState(false);
  const [isHovered, setIsHovered] = useState(false);
  const terrainRef = useRef(null);

  // 用useCallback缓存处理器,避免每次渲染创建新函数
  const clickHandler = useCallback((e) => {
    if (terrainRef.current.contains(e.target)) {
      setIsClicked(prev => !prev);
    } else {
      setIsClicked(false);
    }
  }, []);

  const hoverHandler = useCallback((e) => {
    setIsHovered(terrainRef.current.contains(e.target));
  }, []);

  useEffect(() => {
    document.addEventListener('click', clickHandler);
    document.addEventListener('mouseover', hoverHandler);

    return () => {
      document.removeEventListener('click', clickHandler);
      document.removeEventListener('mouseover', hoverHandler);
    };
  }, [clickHandler, hoverHandler]);

  return (
    <div className={className} id={id}>
      <img src={src} ref={terrainRef} alt={`Terrain ${id}`} />
      {isHovered && <BriefTerrainInfo dataBrief={dataBrief} />}
      {isClicked && <DetailedTerrainInfo dataDetailed={dataDetailed} />}
    </div>
  );
}

内容的提问来源于stack exchange,提问作者Baleandor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 22:15:52