TypeScript中growthCount变量计数超出预期问题求助
我用growthCount变量统计新生成细胞的数量,细胞“分裂”时growthCount自增1,随后通过setState将值存入growthCounts状态变量。该操作在setInterval中按指定间隔重复执行,每次执行时growthCount会重置为0。但持续运行时,growthCount数值经常远超预期——比如模拟中4个细胞分裂本应生成4个新细胞,但growthCount却显示为8。尝试用useRef替代let变量,问题仍存在;但每次分裂后暂停模拟再继续,growthCount就能输出预期值。
相关代码如下:
import "./App.css"; import { useEffect, useState, useRef } from "react"; import PetriDish from "./components/PetriDish"; import Controls from "./components/Controls"; import Graph from "./components/Graph"; function App() { const defaultGridSize: number[] = [20, 20]; // Petri dish vars const [gridSize, setGridSize] = useState<number[]>(defaultGridSize); const [cells, setCells] = useState( Array(defaultGridSize[0]).fill(Array(defaultGridSize[1]).fill(false)) ); const [timeInterval, setTimeInterval] = useState<number>(1); const [running, setRunning] = useState<boolean>(false); const intervalRef = useRef<number>(-1); // Graph vars const [growthCounts, setGrowthCounts] = useState<number[]>([0]); const split = (): void => { let growthCount: number = 0; setCells((prevCells) => { const newCells = prevCells.map((row) => row.slice()); // Loop through each cell for (let i = 0; i < prevCells.length; i++) { for (let j = 0; j < prevCells[i].length; j++) { if (prevCells[i][j]) { // Determine empty cells next to existing bacteria let empty = []; // Left if (j > 0 && !newCells[i][j - 1]) { empty.push([i, j - 1]); } // Right if ( j < prevCells[i].length - 1 && !newCells[i][j + 1] ) { empty.push([i, j + 1]); } // Up if (i > 0 && !newCells[i - 1][j]) { empty.push([i - 1, j]); } // Down if (i < prevCells.length - 1 && !newCells[i + 1][j]) { empty.push([i + 1, j]); } // Split bacteria into an empty cell if (empty.length > 0) { const [a, b] = empty[Math.floor(Math.random() * empty.length)]; newCells[a][b] = true; growthCount++; } } } } return newCells; }); // Write cell count after split to graph setGrowthCounts((prevGrowthCount) => { return [...prevGrowthCount, growthCount]; }); }; const startSim = (): number => { return setInterval(split, timeInterval * 1000); }; useEffect(() => { if (running) { // Error check input fields if (Number.isNaN(timeInterval) || timeInterval < 1) { setRunning(false); alert("Time interval must be at least 1"); } else if ( Number.isNaN(gridSize[0]) || gridSize[0] < 1 || Number.isNaN(gridSize[1]) || gridSize[1] < 1 ) { setRunning(false); alert("Grid size must be at least 1"); } else { intervalRef.current = startSim(); } } else { // Run simulation if (intervalRef.current) { clearInterval(intervalRef.current); } } }, [running]); return ( <main> <PetriDish cells={cells} setCells={setCells} /> <Controls gridSize={gridSize} setGridSize={setGridSize} setCells={setCells} setTimeInterval={setTimeInterval} running={running} setRunning={setRunning} setGrowthCounts={setGrowthCounts} /> <Graph timeInterval={timeInterval} coords={growthCounts} gridSize={gridSize} /> </main> ); } export default App;
问题出在split函数的逻辑里:在setCells的更新回调中,你遍历prevCells的同时修改了newCells,而后续循环会把本次分裂刚生成的新细胞也当成已存在的细胞,触发再次分裂并计数。
比如当4个原始细胞分裂出4个新细胞后,这4个新细胞在同一次循环遍历中会被检测为true,进而再次寻找空细胞分裂,导致growthCount被额外累加,最终得到8。而暂停再继续时,下一次split是基于上一轮更新后的cells状态执行的,新细胞不会在同一次循环里被重复处理,所以数值正常。
核心思路是:只针对原始状态(prevCells)中的细胞执行分裂逻辑,避免同一次循环里处理刚生成的新细胞。可以先收集所有需要分裂的原始细胞位置,再统一处理它们的分裂操作,这样就不会在遍历过程中处理新生成的细胞。
修改后的split函数代码如下:
const split = (): void => { let growthCount: number = 0; setCells((prevCells) => { const newCells = prevCells.map((row) => row.slice()); // 先收集所有原始存在的细胞位置 const existingCells: [number, number][] = []; for (let i = 0; i < prevCells.length; i++) { for (let j = 0; j < prevCells[i].length; j++) { if (prevCells[i][j]) { existingCells.push([i, j]); } } } // 只处理原始存在的细胞,避免同轮处理新生成的细胞 for (const [i, j] of existingCells) { // 寻找当前细胞周围的空细胞 let empty: [number, number][] = []; // Left if (j > 0 && !newCells[i][j - 1]) { empty.push([i, j - 1]); } // Right if (j < prevCells[i].length - 1 && !newCells[i][j + 1]) { empty.push([i, j + 1]); } // Up if (i > 0 && !newCells[i - 1][j]) { empty.push([i - 1, j]); } // Down if (i < prevCells.length - 1 && !newCells[i + 1][j]) { empty.push([i + 1, j]); } // 分裂到空细胞 if (empty.length > 0) { const [a, b] = empty[Math.floor(Math.random() * empty.length)]; newCells[a][b] = true; growthCount++; } } return newCells; }); setGrowthCounts((prevGrowthCount) => { return [...prevGrowthCount, growthCount]; }); };
这样修改后,每次split只会基于触发该次分裂时的原始细胞状态进行处理,同一次循环里不会处理刚生成的新细胞,growthCount就能准确统计单次分裂生成的新细胞数量。
内容的提问来源于stack exchange,提问作者Doma

