30日
30日
React的教程:井字棋游戏
https://zh-hans.react.dev/learn/tutorial-tic-tac-toe
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = 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) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
function calculateWinner(squares) {
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 (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
}
代码讲解
这段代码是一个使用React框架编写的简单井字棋游戏的实现。游戏由一个3x3的棋盘和两个玩家(通常用'X'和'O'表示)组成。每个玩家轮流在棋盘上的空格(square)点击,以尝试连成一线(水平、垂直或对角线)从而赢得游戏。 下面是代码的主要组成部分和它们的功能:
Square
组件:这是一个简单的按钮组件,用于在棋盘上显示玩家的棋子('X'或'O')。Board
组件:这个组件负责渲染整个棋盘。它接收当前玩家的棋子('X'或'O'),当前棋盘的状态(一个包含9个空格值的数组),以及一个当玩家点击棋盘上的空格时应该调用的函数(onPlay
)。handleClick
函数:当玩家点击棋盘上的一个空格时调用。它首先检查该位置是否已被占用或游戏是否已有赢家。如果没有,它会在棋盘数组的相应位置更新玩家的棋子,并调用onPlay
函数来更新游戏状态。calculateWinner
函数:检查棋盘上是否有玩家连成一线。它通过检查棋盘上的所有可能的线(水平、垂直、对角线)来确定是否有赢家,并返回赢家的棋子('X'或'O')。如果没有赢家,它返回null
。
Game
组件:这是游戏的主要组件,它管理游戏的历史记录和当前状态。- 它使用
useState
来跟踪游戏历史(history
),当前移动(currentMove
),以及哪个玩家的回合(xIsNext
)。 handlePlay
函数:当玩家在棋盘上做出移动时调用。它将当前移动的棋盘状态添加到游戏历史中,并更新当前移动。jumpTo
函数:允许玩家跳转到游戏历史中的特定移动。moves
是一个数组,包含游戏历史中每个移动的描述。它使用map
函数来创建一个包含按钮的列表,玩家可以点击这些按钮来回顾游戏的每一步。
- 它使用
calculateWinner
函数(在Game
组件外部定义):这是之前在Board
组件中提到的函数,用于确定是否有玩家赢得了游戏。
整个游戏的UI由Game
组件组织,它显示当前棋盘的状态(通过Board
组件),以及游戏历史(通过一个有序列表显示)。 玩家可以通过点击棋盘上的空格来进行游戏,也可以通过点击历史列表中的按钮来回看之前的移动。游戏的主要逻辑,如检查赢家和更新棋盘状态,都封装在组件的函数和方法中。
需要注意的1
在改变棋盘状态时,并没有直接修改 squares
,而是使用 slice()
复制了一份,这样做的好处是:
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
在JavaScript中,数组是按引用传递的,这意味着当你将一个数组赋值给另一个变量时,两个变量实际上指向内存中的同一个数组对象。因此,如果你修改了这个数组,所有指向这个数组的变量都会反映这些更改。
在React组件Board
中,squares
是一个从父组件Game
通过props传递进来的数组。为了不在原始数组上直接进行更改,避免影响到其他可能使用这个原始数组的组件或逻辑,我们需要创建这个数组的一个副本来进行修改。
squares.slice()
方法在这里被用来创建squares
数组的一个浅拷贝(shallow copy)。这意味着我们创建了一个新的数组对象,它包含了原始数组中的所有元素的引用。当我们修改这个新数组的元素时,不会影响原始数组,因为我们只是修改了副本,而不是原始数组本身。
这样做的好处是:
- 保持不可变性(Immutability):在React中,保持状态的不可变性是一种最佳实践。通过创建状态的副本并修改副本,而不是直接修改状态,我们可以确保组件的渲染是基于最新的、不变的状态。
- 避免副作用(Side Effects):不在原始数组上直接进行更改可以避免潜在的副作用,这些副作用可能会导致难以追踪的错误,尤其是在复杂的应用中。
- 清晰的数据流(Clear Data Flow):通过使用
slice()
创建副本,我们可以清晰地追踪数据是如何在组件之间流动的,以及哪些操作导致了数据的变化。
在handleClick
函数中,我们使用nextSquares[i] = 'X'
或nextSquares[i] = 'O'
来更新副本数组的特定元素。然后,我们通过handlePlay
函数将这个更新后的数组传递给Game
组件,并通过setHistory
更新游戏的历史记录。这样,我们就可以在不影响原始squares
数组的情况下,安全地更新游戏状态。
需要注意的2
使用 handleClick(0)
是行不通的,因为这样直接触发了 handleClick
,而不是点击触发
<Square value={squares[0]} onSquareClick={handleClick(0)} />
应该改成 () => handleClick(0)
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
- 0
- 0
- 0
- 0
- 0
- 0