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

如何调试组件渲染无限循环?TP功能触发时出现异常

问题描述

当checkCircle函数的"TP"分支执行时,组件出现无限渲染循环异常。尝试改用本地state替代Redux修改状态后,问题仍然存在。该分支需要实现玩家位置与地点的瞬移更新。

已移除无关代码

Map.js

import CheckCircle from "./../../Services/checkCircle";
import { useDispatch, useSelector } from "react-redux";
import { fetchPlayer } from "./../../Services/fetchPlayerAsync";
import { updatePlayerPositionAction } from "./../../Store/playerReduser";

export default function Map({ circles }) {
  const [position, setPosition] = useState([-100, 100]);
  const [previousPosition, setPreviousPosition] = useState([-100, 100]);
  const [mapCenter, setMapCenter] = useState([-100, 100]);
  const [isInside, setIsInside] = useState(false);
  const [currentClassName, setCurrentClassName] = useState("");
  const [keyPressed, setKeyPressed] = useState(null);
  const [location, setLocation] = useState("");
  const dispatch = useDispatch();
  const player = useSelector((state) => state.players.players);

  useEffect(() => {
    dispatch(fetchPlayer());
  }, [dispatch]);

  useEffect(() => {
    if (player && player.player) {
      setLocation(player.player.location);
      setPosition(player.player.position);
      setPreviousPosition(player.player.prevPosition);
      setMapCenter(player.player.position);
    }
  }, [player]);

  const keyDownRead = (e) => {
    setKeyPressed(e.code);
  };

  useEffect(() => {
    window.addEventListener("keydown", keyDownRead);
    return () => {
      window.removeEventListener("keydown", keyDownRead);
    };
  }, []);

  useEffect(() => {
    let shortestDistance = Infinity;
    let currentisInside = false;
    let currentClassName = "";

    Object.entries(circles).forEach(([key, value]) => {
      const distance = Math.sqrt(
        (position[0] - value.center[0]) ** 2 +
          (position[1] - value.center[1]) ** 2
      );

      if (distance <= value.radius) {
        currentisInside = true;
        currentClassName = value.className;
      } else if (distance < shortestDistance) {
        shortestDistance = distance;
      }
    });

    setCurrentClassName(currentClassName);
    setIsInside(currentisInside);
  }, [position, circles]);

  return (
    <>
      {location.length > 0 ? (
        <div className="App">
          <MapContainer
            maxBounds={bounds[location]}
            dragging={true}
            boxZoom={false}
            center={mapCenter}
            minZoom={1}
            maxZoom={4}
            zoom={4}
            style={{
              height: window.innerHeight,
              width: window.innerWidth,
            }}
            crs={CRS.Simple}
            scrollWheelZoom={true}
            attributionControl={false}
          >
            <ChangeView center={position} zoom={4} />
            <TileLayer
              attribution="it’s offline"
              url="../tiles/{z}/{x}/{y}.png"
            />
            {Object.entries(circles).map(([key, value]) => (
              <Circle key={key} center={value.center} radius={value.radius}>
                {isInside &&
                  value.className === currentClassName &&
                  CheckCircle(value.className, keyPressed, dispatch)}
              </Circle>
            ))}
            <Marker position={position} title="player" />
            <GetCoordinates />
            {Object.entries(borders).map(([key, value]) => (
              <Polyline key={key} positions={borders[key]} />
            ))}
          </MapContainer>
        </div>
      ) : null}
    </>
  );
}

CheckCircle.js

import NPCdialog from "../Components/PlayGround/NPCdialog";
import { updatePlayerPositionLocationAction } from "../Store/playerReduser";

export default function checkCircle(circle, keyPressed, dispatch) {
  if (!circle) return;
  let event = circle.split("_")[0];
  let item1 = circle.split("_")[1];
  let item2 = circle.split("_")[2];
  switch (event) {
    case "NPC":
      if (keyPressed === "KeyE") {
        return <NPCdialog npcName={item1} />;
      }
      break;
    case "Fight":
      return null;
    case "TP":
      if(keyPressed === "KeyE") {
        const pos = JSON.parse("[" + item1 + "]");
        dispatch(updatePlayerPositionLocationAction(pos, pos, item2));
        return null;
      }
      break;
    default:
      return null;
  }
}

问题分析与解决方案

核心问题原因

  1. 渲染阶段触发状态更新:CheckCircle在组件render流程中执行,调用dispatch会直接触发Redux状态变更,引发组件重新渲染,而重新渲染又会再次执行CheckCircle并触发dispatch,形成无限循环。
  2. 按键状态未重置:按下"KeyE"后keyPressed一直保持该值,后续每次渲染都会满足TP分支的触发条件,重复执行状态更新。
  3. 状态同步连锁反应:Redux状态更新后,监听player的useEffect会同步本地state,进一步触发依赖position的useEffect,加剧循环。

修复方案

1. 调整TP逻辑触发时机,移到事件回调中

将瞬移的dispatch逻辑从渲染阶段移到键盘事件回调里,避免渲染时触发状态更新。

2. 重置按键状态

处理完TP逻辑后立即清空keyPressed,防止重复触发。

3. 拆分CheckCircle职责

让CheckCircle只负责UI组件渲染,状态修改逻辑单独处理。

修改后代码示例

Map.js 修改版

import CheckCircle from "./../../Services/checkCircle";
import { useDispatch, useSelector } from "react-redux";
import { fetchPlayer } from "./../../Services/fetchPlayerAsync";
import { updatePlayerPositionAction } from "./../../Store/playerReduser";

export default function Map({ circles }) {
  const [position, setPosition] = useState([-100, 100]);
  const [previousPosition, setPreviousPosition] = useState([-100, 100]);
  const [mapCenter, setMapCenter] = useState([-100, 100]);
  const [isInside, setIsInside] = useState(false);
  const [currentClassName, setCurrentClassName] = useState("");
  const [keyPressed, setKeyPressed] = useState(null);
  const [location, setLocation] = useState("");
  const dispatch = useDispatch();
  const player = useSelector((state) => state.players.players);

  useEffect(() => {
    dispatch(fetchPlayer());
  }, [dispatch]);

  useEffect(() => {
    if (player && player.player) {
      setLocation(player.player.location);
      setPosition(player.player.position);
      setPreviousPosition(player.player.prevPosition);
      setMapCenter(player.player.position);
    }
  }, [player]);

  const keyDownRead = (e) => {
    const pressedKey = e.code;
    setKeyPressed(pressedKey);

    // 直接在事件回调中处理TP逻辑
    if (isInside && currentClassName && pressedKey === "KeyE") {
      const [event, item1, item2] = currentClassName.split("_");
      if (event === "TP") {
        const pos = JSON.parse("[" + item1 + "]");
        dispatch(updatePlayerPositionLocationAction(pos, pos, item2));
        // 重置按键状态,避免重复触发
        setKeyPressed(null);
      }
    }
  };

  useEffect(() => {
    window.addEventListener("keydown", keyDownRead);
    return () => {
      window.removeEventListener("keydown", keyDownRead);
    };
  }, [isInside, currentClassName, dispatch]); // 添加依赖确保获取最新状态

  useEffect(() => {
    let shortestDistance = Infinity;
    let currentisInside = false;
    let currentClassName = "";

    Object.entries(circles).forEach(([key, value]) => {
      const distance = Math.sqrt(
        (position[0] - value.center[0]) ** 2 +
          (position[1] - value.center[1]) ** 2
      );

      if (distance <= value.radius) {
        currentisInside = true;
        currentClassName = value.className;
      } else if (distance < shortestDistance) {
        shortestDistance = distance;
      }
    });

    setCurrentClassName(currentClassName);
    setIsInside(currentisInside);
  }, [position, circles]);

  return (
    <>
      {location.length > 0 ? (
        <div className="App">
          <MapContainer
            maxBounds={bounds[location]}
            dragging={true}
            boxZoom={false}
            center={mapCenter}
            minZoom={1}
            maxZoom={4}
            zoom={4}
            style={{
              height: window.innerHeight,
              width: window.innerWidth,
            }}
            crs={CRS.Simple}
            scrollWheelZoom={true}
            attributionControl={false}
          >
            <ChangeView center={position} zoom={4} />
            <TileLayer
              attribution="it’s offline"
              url="../tiles/{z}/{x}/{y}.png"
            />
            {Object.entries(circles).map(([key, value]) => (
              <Circle key={key} center={value.center} radius={value.radius}>
                {/* 仅保留NPC的UI渲染逻辑 */}
                {isInside &&
                  value.className === currentClassName &&
                  keyPressed === "KeyE" &&
                  value.className.split("_")[0] === "NPC" &&
                  CheckCircle(value.className, keyPressed)}
              </Circle>
            ))}
            <Marker position={position} title="player" />
            <GetCoordinates />
            {Object.entries(borders).map(([key, value]) => (
              <Polyline key={key} positions={borders[key]} />
            ))}
          </MapContainer>
        </div>
      ) : null}
    </>
  );
}

CheckCircle.js 修改版

import NPCdialog from "../Components/PlayGround/NPCdialog";

export default function checkCircle(circle, keyPressed) {
  if (!circle) return;
  const [event, item1] = circle.split("_");
  
  switch (event) {
    case "NPC":
      if (keyPressed === "KeyE") {
        return <NPCdialog npcName={item1} />;
      }
      break;
    case "Fight":
    case "TP":
    default:
      return null;
  }
}

关键修改说明

  • 将TP的状态更新逻辑从渲染流程中移除,放到键盘事件回调内,彻底避免渲染时触发循环。
  • 处理完瞬移后立即重置keyPressed,防止后续渲染重复触发。
  • 简化CheckCircle的职责,只保留NPC对话框的渲染逻辑,分离状态修改操作。
  • 更新键盘事件监听的依赖数组,确保回调能获取最新的isInside和currentClassName状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 23:02:33