用React实现井棋棋游戏:从状态管理到时间旅行功能详解
引言
井字棋是学习React状态管理和组件交互的经典案例。本文将逐行解析一个完整实现,包含历史回溯功能和胜者判定逻辑,即使是React初学者也能彻底理解。
核心组件架构
1. Square组件:最基础的交互单元
function Square({ value, onSquareClick }) {
return (
<button onClick={onSquareClick}>
{value}
</button>
);
}
- 功能解析:
value
:接收父组件传递的X/O/null
onSquareClick
:点击时触发父组件的回调函数- 设计要点:组件本身不维护状态,由父组件完全控制(受控组件)
2. Board组件:游戏逻辑核心
function Board({ xIsNext, square, onPlay }) {
// 处理格子点击事件
function handleClick(i) {
// 存在胜者或格子已被占用时阻止操作
if (calculateWinner(square) || square[i]) return;
const nextSquares = square.slice(); // 浅拷贝当前棋盘状态
nextSquares[i] = xIsNext ? 'X' : 'O'; // 设置当前玩家符号
onPlay(nextSquares); // 传递新状态给父组件
}
// 判断胜负状态
const winner = calculateWinner(square);
const status = winner
? `Winner: ${winner}`
: `Next player: ${xIsNext ? 'X' : 'O'}`;
// 渲染9宫格棋盘
return (
<>
<div className="status">{status}</div>
{[0, 3, 6].map((rowStart) => (
<div key={rowStart} className="board-row">
{[0, 1, 2].map((offset) => {
const index = rowStart + offset;
return (
<Square
key={index}
value={square[index]}
onSquareClick={() => handleClick(index)}
/>
);
})}
</div>
))}
</>
);
}
关键逻辑解析:
- 点击处理流程
- 通过
square[i]
检查格子是否为空 - 使用
slice()
浅拷贝数组避免直接修改状态 - 根据
xIsNext
决定放置X
或O
- 通过
- 状态提升(Lifting State Up)
- 通过
onPlay
将新状态回调给Game组件 - 符合React单向数据流原则
- 通过
3. Game组件:全局状态管理
export default function Game() {
// 历史记录保存所有棋盘状态
const [history, setHistory] = useState([Array(9).fill(null)]);
// 当前查看的历史步骤
const [currentMove, setCurrentMove] = useState(0);
// 派生状态
const xIsNext = currentMove % 2 === 0;
const currentSquare = history[currentMove];
// 处理棋盘状态更新
function handlePlay(nextSquares) {
// 裁剪历史记录(用于时间旅行后重新落子)
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
// 时间旅行:跳转到指定历史步骤
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// 生成历史步骤按钮
const moves = history.map((squares, move) => {
const desc = move > 0
? `Go to move #${move}`
: 'Go to game start';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board
xIsNext={xIsNext}
square={currentSquare}
onPlay={handlePlay}
/>
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
核心机制解析:
- 历史记录管理
history
:存储棋盘状态数组的数组currentMove
:当前展示的历史步骤索引
- 时间旅行实现原理
- 点击历史按钮时
jumpTo
修改currentMove
- 渲染对应历史索引的状态:
currentSquare = history[currentMove]
- 重新落子时裁剪历史分支:
history.slice(0, currentMove + 1)
确保历史线性的正确性
- 点击历史按钮时
4. 胜者判定算法
function calculateWinner(squares) {
// 8种获胜组合(三行/三列/两对角线)
const lines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // 行
[0, 3, 6], [1, 4, 7], [2, 5, 8], // 列
[0, 4, 8], [2, 4, 6] // 对角线
];
for (const [a, b, c] of lines) {
if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]
) {
return squares[a]; // 返回胜者符号'X'/'O'
}
}
return null; // 无胜者
}
算法特点:
- 时间复杂度:O(1)(固定8种判断)
- 返回值:
'X'/'O'
(胜者)或null
(未结束) - 空值检查:
squares[a] &&
防止访问未填充格子
总结
组件层级清晰
- Game(状态容器)→ Board(逻辑核心)→ Square(UI展示)
符合React最佳实践:
- 状态提升:子组件通过回调修改父组件状态
- 不可变数据:使用
slice()
创建新数组 - 派生状态:
xIsNext
由currentMove
计算得出
进阶功能实现:
- 时间旅行:通过历史索引切换棋盘状态
- 历史裁剪:重新落子时自动清理分叉历史
可扩展方向:
通过这个实现,我们展示了React的核心概念:组件化、状态提升、不可变数据和派生状态。即使从未接触过React,按照组件分解的思维方式也能逐步理解整个应用的数据流动和交互逻辑。