Я создаю реакцию + машинопись + вайт Nonogram App.
Вы можете увидеть недостаточную производительность для больших размеров сетки в живой демонстрации ghpages: https://timonkobusch.github.io/nonogram/ Вот полный код для него: https://github.com/timonkobusch/nonogram
Мой вопрос и идеи:
Вот два наиболее важных файла, рендеринг которых (очевидно) занимает больше всего времени: Нонограммагрид.tsx:
const NonogramGrid = ({
nonogram,
onMouseDownHandler,
onMouseOverHandler,
gameRunning,
}: INonogramGridProps) => {
const [selectedCell, setSelectedCell] = useState<{
row: number;
column: number;
}>({ row: -1, column: -1 });
const handleMouseDown =
(row: number, col: number) => (e: React.MouseEvent) => {
e.preventDefault();
if (!gameRunning || nonogram.progress.isWon) return;
onMouseDownHandler(row, col);
};
const handleMouseOver =
(row: number, col: number) => (e: React.MouseEvent) => {
e.preventDefault();
if (!gameRunning || nonogram.progress.isWon) return;
setSelectedCell({ row, column: col });
onMouseOverHandler(row, col);
};
const handleMouseLeave = () => {
setSelectedCell({ row: -1, column: -1 });
};
const validSizes = [5, 10, 15, 20, 25];
const sizeClass = validSizes.includes(nonogram.size)
? `grid-${nonogram.size}x${nonogram.size}`
: "grid-15x15";
return (
<div className = {`content ${sizeClass}`}>
<Hints
hintLines = {nonogram.hints.rows}
finishedLines = {nonogram.finishedLines.rows}
gameRunning = {gameRunning && !nonogram.progress.isWon}
classIdentifier = {"left-hints"}
/>
<table>
<Hints
hintLines = {nonogram.hints.columns}
finishedLines = {nonogram.finishedLines.columns}
gameRunning = {gameRunning && !nonogram.progress.isWon}
classIdentifier = {"top-hints"}
/>
<tbody className = "table">
{Array.from({ length: nonogram.size }).map((_, row) => {
return (
<tr key = {row} className = "row">
{Array.from({ length: nonogram.size }).map(
(_, col) => {
const fifthRowBorder =
row === 4 ? "fifth-row" : "";
const highlighted =
selectedCell.row === row ||
selectedCell.column === col
? "highlighted"
: "";
const hideCell = gameRunning
? ""
: "hide-cell";
const gameWon = nonogram.progress.isWon
? "game-won"
: "";
let colored = "empty";
switch (nonogram.grid[row][col]) {
case 1:
colored = "colored";
break;
case 2:
colored = "crossed";
break;
case 3:
colored = "wrongColored";
break;
case 4:
colored = "wrongCrossed";
break;
}
return (
<td
key = {col}
className = {`cell ${fifthRowBorder}`}
onMouseDown = {handleMouseDown(
row,
col
)}
onMouseOver = {handleMouseOver(
row,
col
)}
onMouseLeave = {handleMouseLeave}
>
{row > 0 && row % 5 === 0 && (
<div className = "fifth-row-border"></div>
)}
{col > 0 && col % 5 === 0 && (
<div className = "fifth-col-border"></div>
)}
<div
id = "cell"
className = {`${colored} ${highlighted} ${hideCell} ${gameWon}`}
></div>
</td>
);
}
)}
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default NonogramGrid;
И App.tsx, который обрабатывает состояние игры и нонограмму:
import "components/App.scss";
import NonogramGrid from "./NonogramGrid/NonogramGrid";
import { Nonogram } from "modules/Nonogram";
import { useEffect, useState } from "react";
import GameController from "components/GameController/GameController";
import PlayController from "components/PlayController/PlayController";
import useTimer from "components/utils/useTimer";
import AppHeader from "components/AppHeader/AppHeader";
import About from "components/About/About";
import workerURL from "./utils/createClassWorker?worker&url";
const enum MarkLock {
UNSET = 0,
ROW = 1,
COL = 2,
}
const App = () => {
const [nonogram, setNonogram] = useState(() => new Nonogram(10));
const [nonogramHistory, setNonogramHistory] = useState<Nonogram[]>([]);
const [mouseDown, setMouseDown] = useState(false);
const [marking, setMarking] = useState(true);
const [loading, setLoading] = useState(false);
const [clearMode, setClearMode] = useState(false);
const { seconds, resetTimer, startTimer, pauseTimer } = useTimer();
const [gameRunning, setgameRunning] = useState(false);
const [clickCoords, setClickCoords] = useState({ row: 0, column: 0 });
const [markLock, setMarkLock] = useState(MarkLock.UNSET);
useEffect(() => {
const handleMouseUp = () => {
if (mouseDown) {
setMouseDown(false);
}
};
const handleFKeyPress = (e: KeyboardEvent) => {
if (e.key === "f") {
setMarking(!marking);
}
};
document.addEventListener("mouseup", handleMouseUp);
document.addEventListener("keypress", handleFKeyPress);
return () => {
document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("keypress", handleFKeyPress);
};
});
const handleUndo = () => {
...
};
const handleMouseDown = (x: number, y: number) => {
...
};
const handleMouseOver = (x: number, y: number) => {
...
};
const handleGenerate = (size: number) => {
...
};
const handleReset = () => {
...
};
const handlePause = () => {
...
};
return (
<div className = "App">
<AppHeader />
<div className = "App-content">
<div className = "ControlField">
<GameController
handleGenerate = {handleGenerate}
handleReset = {handleReset}
gameWon = {nonogram.progress.isWon}
loading = {loading}
/>
<PlayController
progress = {nonogram.progress}
seconds = {seconds}
handlePause = {handlePause}
gameRunning = {gameRunning}
handleUndo = {handleUndo}
undoActive = {nonogramHistory.length > 0}
marking = {marking}
toggleMarking = {() => setMarking(!marking)}
/>
<About />
</div>
<NonogramGrid
nonogram = {nonogram}
onMouseDownHandler = {handleMouseDown}
onMouseOverHandler = {handleMouseOver}
gameRunning = {gameRunning}
/>
</div>
</div>
);
};
export default App;
Я искал проблему и в основном находил решения с помощью окна реакции, но здесь это не решение, потому что сетка всегда полностью видна.
Если у вас есть опыт работы с этим, не могли бы вы дать мне указания, как вы это решили? Является ли использование библиотеки анимации решением?
🤔 А знаете ли вы, что...
React поддерживает однонаправленный поток данных (unidirectional data flow) для предсказуемого управления состоянием.
Я вижу, что есть проблема с таймером - таймер реализован внутри хука useTimer, который импортируется в компонент приложения. При каждом перерисовке пользовательского перехвата компонент, в который импортируется перехват, также перерисовывается. Это означает, что каждую секунду происходит новый рендеринг. Я написал код, запоминающий компонент Nonogramgrid, потому что там много дорогостоящих итераций. Вот изменения:
const memoizedNonogramGrid = useMemo(() => {
console.info("rerender the grid")
return (
<NonogramGrid
nonogram = {nonogram}
onMouseDownHandler = {handleMouseDown}
onMouseOverHandler = {handleMouseOver}
gameRunning = {gameRunning}
/>
)
}, [gameRunning, handleMouseDown, handleMouseOver, nonogram])
const handleMouseDown = useMemo(() => {
return (x: number, y: number) => {
setNonogramHistory([...nonogramHistory, new Nonogram(nonogram)]);
const updatedGrid = new Nonogram(nonogram);
updatedGrid.click(x, y, marking);
setNonogram(updatedGrid);
// Set mouse state
setMouseDown(true);
setClickCoords({ row: y, column: x });
setMarkLock(MarkLock.UNSET);
if (nonogram.grid[x][y] === 0) {
setClearMode(false);
} else {
setClearMode(true);
}
if (updatedGrid.progress.isWon) {
pauseTimer();
}
};
}, [nonogram, marking, nonogramHistory, pauseTimer])
const handleMouseOver = useMemo(() => {
return (x: number, y: number) => {
if (mouseDown) {
if (x === clickCoords.column && y === clickCoords.row) {
return;
}
if (markLock === MarkLock.UNSET) {
if (x === clickCoords.column) {
setMarkLock(MarkLock.COL);
} else if (y === clickCoords.row) {
setMarkLock(MarkLock.ROW);
}
}
if (markLock === MarkLock.COL) {
x = clickCoords.column;
} else if (markLock === MarkLock.ROW) {
y = clickCoords.row;
}
const updatedGrid = new Nonogram(nonogram as Nonogram);
updatedGrid.setCell(x, y, marking, clearMode);
setNonogram(updatedGrid);
if (updatedGrid.progress.isWon) {
pauseTimer();
}
}
};
}, [clearMode, clickCoords.column, clickCoords.row, markLock, nonogram, mouseDown, marking, pauseTimer])
const pauseTimer = useMemo(() => {
return () => {
setActive(false);
};
}, [])
Конечно, запоминание можно выполнить с помощью хука useCallback.
Второе изменение, которое я сделал, — это передача начального значения в состояние нонограммы, поскольку хорошей практикой является передача не функции, а только значения, поскольку при каждом повторном рендеринге происходит выполнение этой функции. Другими словами, ЦП создает новую нонограмму (10). Вот код:
const initialNonogramState = new Nonogram(10)
const App = () => {
const [nonogram, setNonogram] = useState(initialNonogramState);
Далее, по-прежнему существует проблема с компонентом сетки Nonogram — при каждом наведении курсора мыши происходит повторный рендеринг компонента, и каждый повторный рендеринг создает новые итерации. Сейчас я рассматриваю некоторую логику для создания компонентов и простого изменения реквизита. Меня вдохновила библиотека React-Data-Table-Component, так как у них есть хорошая функция запоминания таблиц.