如何调试组件渲染无限循环?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; } }
问题分析与解决方案
核心问题原因
- 渲染阶段触发状态更新:
CheckCircle在组件render流程中执行,调用dispatch会直接触发Redux状态变更,引发组件重新渲染,而重新渲染又会再次执行CheckCircle并触发dispatch,形成无限循环。 - 按键状态未重置:按下"KeyE"后
keyPressed一直保持该值,后续每次渲染都会满足TP分支的触发条件,重复执行状态更新。 - 状态同步连锁反应: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
相关产品推荐
相关产品推荐

