0%

用 React 实现一个井字棋游戏(二)

@pftom

查看代码

使用函数式组件

为什么不变性是很重要的

在之前的代码例子中,我们建议你使用 .slice() 方法来创建 squares 的一份拷贝而不是直接修改它。我们将马上讨论不变形以及为什么不变形是值得学习的。

一般有两种方式修改数据。第一种方法就是通过直接修改数据的值让数据产生突变。第二种方法是在数据的拷贝上做修改然后替代原数据。

突变式的数据修改:

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// Now player is {score: 2, name: 'Jeff'}

非突变式的数据修改:

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// 现在 player 没有被修改,但是 newPlayer 的值却变成了 {score: 2, name: 'Jeff'}

// 或者你想使用对象结构语法,你可以这样写:
// var newPlayer = {...player, score: 2};

即使没有突变的修改数据,最终的结果却是一样的,我们马上来说一下这样做的几个好处。

复杂的功能变简单

不可变性使得复杂的功能变得很容易实现。在这篇教程的后面,我们将会实现一个 “时间旅行” 功能,它使得我们的 “井字棋” 游戏能显示游戏的历史,并且可以 “跳回” 到之前的步骤。这个功能不是这个游戏才有的 – 拥有 “撤销” 和 “重做” 功能是目前应用的一个很通用的需求。避免直接对数据进行突变式的修改使得我们能够保存先前版本游戏记录的完整历史,并在之后重新使用。

追踪改变

追踪改变对于突变的对象来说是很困难的因为它们直接被修改了。我们需要遍历整个对象树,然后比较突变对象和它之前的拷贝来追踪这种变化。

追踪改变对于不变的对象来说是相当简单的。如果这个不变的对象的引用和之前的不一样了,那么它就改变了。

判断什么时候该在 React 中重新渲染

不变形的主要优点是它帮助你在 Reac 构建纯组件。不变的数据能很容易判断是否发生了变化,我们以此来决定一个组件是否需要重新渲染。

你能 shouldComponentUpdate() 学到更多关于不变性的知识,以及通过阅读性能优化来学习如何构建纯组件。

什么是函数式组件

我们将马上将 Square 改成一个函数式组件。

在 React 中,函数式组件是一种编写组件的简单写法,你只需要包含 render 方法里面的内容,并且不需要 state 。我们通过编写函数,接收 props 作为输入,然后返回需要渲染的内容来定义组件,而不是之前通过定义继承自 React.Component 的类来定义组件。函数式组件没有类组件那么冗长,并且大多数的组件都能通过这样的方式来定义。

用下面的这个函数来取代 Square 类:

function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}

我们已经将两处 this.props 出现的位置改成了 props

最终我们的代码变成了这样:

src/index.js查看完整代码
// ...
import ReactDOM from "react-dom";
import "./index.css";

[tuture-del]class Square extends React.Component {
[tuture-del] render() {
[tuture-del] return (
[tuture-del] <button className="square" onClick={() => this.props.onClick()}>
[tuture-del] {this.props.value}
[tuture-del] </button>
[tuture-del] );
[tuture-del] }
[tuture-add]function Square(props) {
[tuture-add] return (
[tuture-add] <button className="square" onClick={() => props.onClick()}>
[tuture-add] {props.value}
[tuture-add] </button>
[tuture-add] );
}

class Board extends React.Component {
// ...

查看此时的所有项目代码

注意当我们将 Square 修改成函数式组件之后,我们也将 onClick={() => this.props.onClick()} 改成了更短的 onClick={props.onClick} (注意到大括号内的前后都没有了圆括号)

开始翻转

现在我们需要修复之前的缺陷 – 在之前我们的 “井字棋” 游戏中,“O“ 出现不了。

我们将把第一步移动默认设置为 “X”。我们可以在 Board 的 constructor 里面修改它:

class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}

每当一个选手移动一步,xIsNext (一个布尔值)将会翻转来判断下一个选手将会出现什么,并且翻转后的状态会被保存在 state 里面。我们将修改 Board 的 handleClick 函数来翻转 xIsNext 的值:

handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}

当保存上面的改变之后,你的 Board 组件看起来应该是这样的:

src/index.js查看完整代码
// ...
constructor(props) {
super(props);
this.state = {
[tuture-del] squares: Array(9).fill(null)
[tuture-add] squares: Array(9).fill(null),
[tuture-add] xIsNext: true
};
}

handleClick(i) {
const squares = this.state.squares.slice();
[tuture-del] squares[i] = "X";
[tuture-del] this.setState({ squares: squares });
[tuture-add] squares[i] = this.state.xIsNext ? "X" : "O";
[tuture-add] this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
}

renderSquare(i) {
// ...
}

render() {
[tuture-del] const status = "Next player: X";
[tuture-add] const status = "Next player: " + (this.state.xIsNext ? "X" : "O");

return (
<div>
// ...

查看此时的所有项目代码

显示获胜者

保存上面所做的修改,目前你的 src/index.js 的内容应该看起来是这样子的:

src/index.js查看完整代码
// ...

handleClick(i) {
const squares = this.state.squares.slice();
[tuture-add]
[tuture-add] if (calculateWinner(squares) || squares[i]) {
[tuture-add] return;
[tuture-add] }
[tuture-add]
squares[i] = this.state.xIsNext ? "X" : "O";
this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
}
// ...
}

render() {
[tuture-del] const status = "Next player: " + (this.state.xIsNext ? "X" : "O");
[tuture-add] const winner = calculateWinner(this.state.squares);
[tuture-add] let status;
[tuture-add] if (winner) {
[tuture-add] status = "Winner: " + winner;
[tuture-add] } else {
[tuture-add] status = "Next player: " + (this.state.xIsNext ? "X" : "O");
[tuture-add] }

return (
<div>
// ...
// ========================================

ReactDOM.render(<Game />, document.getElementById("root"));
[tuture-add]
[tuture-add]function calculateWinner(squares) {
[tuture-add] const lines = [
[tuture-add] [0, 1, 2],
[tuture-add] [3, 4, 5],
[tuture-add] [6, 7, 8],
[tuture-add] [0, 3, 6],
[tuture-add] [1, 4, 7],
[tuture-add] [2, 5, 8],
[tuture-add] [0, 4, 8],
[tuture-add] [2, 4, 6]
[tuture-add] ];
[tuture-add] for (let i = 0; i < lines.length; i++) {
[tuture-add] const [a, b, c] = lines[i];
[tuture-add] if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
[tuture-add] return squares[a];
[tuture-add] }
[tuture-add] }
[tuture-add] return null;
[tuture-add]}

查看此时所有项目代码

恭喜你!你现在已经有一个可以运行的 “井字棋” 游戏了。并且你还学习了 React 的基础知识。所以你可能是这里面真正的赢家!

再次状态提升

作为最后的练习,让我们使得游戏 “回到过去” 变得可能。

保存移动的历史

如果我们突变式的修改了 squares 数组,实现时间旅行就会变得很困难。

然而,我们在每步移动都使用了 slice() 来创建 squares 数组的一份新的拷贝,将其看做是不可变的。这使得我们可以保存 squares 数组的每一步历史版本,然后在在已经发生的移动历史之间跳转。

我们将把 squares 数组的历史版本保存到另外一个 history 数组中。history 代表了从游戏开始到上一步移动所有棋盘格的状态。它看起来是下面这样的:

history = [
// Before first move
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// After first move
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// After second move
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]

现在我们需要思考,那个组件应该保存 history 状态。

我们想让顶层的 Game 组件来展示历史移动列表。这需要获取 history 才能做到,所以我们把 history 移动到顶层的 Game 组件的 state 中来。

history 状态移动到 Game 组件中之后,我们需要将 squares state 从它的子组件 Board 中删除。就像我们之前将 Square 组件中的状态提升到 Board 组件中一样,我们现在将状态从 Board 组件提升到顶层的 Game 组件中。这使得 Game 组件完全控制了 Board 的数据,并且使得它可以操纵 Board 组件来渲染之前的移动状态。

首先,我们将在 Game 的 constructor 中来设置它的初始状态:

class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
xIsNext: true,
};
}

render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}

接下来,我们将修改 Board 组件,使得它接收来自 Game 组件的 squaresonClick props。因为目前我们在 Board 组件上定义了唯一的一个点击处理函数来处理众多的 Square 的点击事件,所以为了指示那个 Square 被点击了,我们需要通过它的 onClick 事件处理函数传递它所在的位置。下面是迁移 Board 组件所必须的步骤:

  • 删除 Board 里面的 constructor
  • 在 Board 的 renderSquare 里面将 this.state.squares[i] 替换层 this.props.squares[i]
  • 在 Board 的 renderSquare 里面将 this.handleClick(i) 替换成 this.props.handleClick(i)

Board 组件现在看起来是这样的:

class Board extends React.Component {
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}

renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}

render() {
const winner = calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}

return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}

我们将更新 Game 组件的 render 函数来使用最新历史记录,并通过这一历史记录来展示游戏的状态:



因为 Game 组件目前已经渲染了游戏的状态,我们可以删除 Board 组件的 render 方法里面对于的代码了。当我们重构完,Board 的 render 方法看起来像这样:

render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);

let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}

return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}

最终,我们需要把 Board 里面的 handleClick 方法移动到 Game 组件中。我们还需要对 handleClick 做出一点修改,因为 Game 组件的 state 结构不一样。在 Game 的 handleClick 方法中,我们将新的历史记录条目添加到 history 中。

handleClick(i) {
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}

注意不像你熟悉的 push() 方法,concat() 方法不会突变式的修改原数组,所以我们更青睐它。

现在,Board 组件只需要 renderSquarerender 方法了。游戏的状态和 handleClick 方法都定义在 Game 组件中。

当保存上面的修改之后,现在我们的 src/index.js 应该是这样的:

src/index.js查看完整代码
// ...
}

class Board extends React.Component {
[tuture-del] constructor(props) {
[tuture-del] super(props);
[tuture-del] this.state = {
[tuture-del] squares: Array(9).fill(null),
[tuture-del] xIsNext: true
[tuture-del] };
[tuture-del] }
[tuture-del]
[tuture-del] handleClick(i) {
[tuture-del] const squares = this.state.squares.slice();
[tuture-del]
[tuture-del] if (calculateWinner(squares) || squares[i]) {
[tuture-del] return;
[tuture-del] }
[tuture-del]
[tuture-del] squares[i] = this.state.xIsNext ? "X" : "O";
[tuture-del] this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
[tuture-del] }
[tuture-del]
renderSquare(i) {
return (
<Square
[tuture-del] value={this.state.squares[i]}
[tuture-del] onClick={() => this.handleClick(i)}
[tuture-add] value={this.props.squares[i]}
[tuture-add] onClick={() => this.props.onClick(i)}
/>
);
}

render() {
[tuture-del] const winner = calculateWinner(this.state.squares);
[tuture-del] let status;
[tuture-del] if (winner) {
[tuture-del] status = "Winner: " + winner;
[tuture-del] } else {
[tuture-del] status = "Next player: " + (this.state.xIsNext ? "X" : "O");
[tuture-del] }
[tuture-del]
return (
<div>
[tuture-del] <div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
// ...
}

class Game extends React.Component {
[tuture-add] constructor(props) {
[tuture-add] super(props);
[tuture-add] this.state = {
[tuture-add] history: [
[tuture-add] {
[tuture-add] squares: Array(9).fill(null)
[tuture-add] }
[tuture-add] ],
[tuture-add] xIsNext: true
[tuture-add] };
[tuture-add] }
[tuture-add]
[tuture-add] handleClick(i) {
[tuture-add] const history = this.state.history;
[tuture-add] const current = history[history.length - 1];
[tuture-add] const squares = current.squares.slice();
[tuture-add]
[tuture-add] if (calculateWinner(squares) || squares[i]) {
[tuture-add] return;
[tuture-add] }
[tuture-add]
[tuture-add] squares[i] = this.state.xIsNext ? "X" : "O";
[tuture-add] this.setState({
[tuture-add] history: history.concat([
[tuture-add] {
[tuture-add] squares: squares
[tuture-add] }
[tuture-add] ]),
[tuture-add] xIsNext: !this.state.xIsNext
[tuture-add] });
[tuture-add] }
[tuture-add]
render() {
[tuture-add] const history = this.state.history;
[tuture-add] const current = history[history.length - 1];
[tuture-add] const winner = calculateWinner(current.squares);
[tuture-add]
[tuture-add] let status;
[tuture-add] if (winner) {
[tuture-add] status = "Winner: " + winner;
[tuture-add] } else {
[tuture-add] status = "Next player: " + (this.state.xIsNext ? "X" : "O");
[tuture-add] }
[tuture-add]
return (
<div className="game">
<div className="game-board">
[tuture-del] <Board />
[tuture-add] <Board squares={current.squares} onClick={i => this.handleClick(i)} />
</div>
<div className="game-info">
[tuture-del] <div>{/* status */}</div>
[tuture-add] <div>{status}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
// ...

查看此时所有项目代码

展示移动历史

因为我们记录了井字棋游戏的历史,我们现在可以将这份历史列表展示给玩家。

我们之前学到,React 元素是 JavaScript 对象;我们可以在应用中传递它们。为了渲染多个元素,我们可以一个 React 元素数组。

在 JavaScript 中,数组有一个 map() 方法,普遍的用于将一份数据映射到另外一份数据,例如:

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

使用 map 方法,我们可以将移动的历史映射成展示在屏幕上的 React button 元素。

让我们在 Game 的 render 方法里来 map 我们的 history

src/index.js查看完整代码
// ...
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);

[tuture-add] const moves = history.map((step, move) => {
[tuture-add] const desc = move ? "Go to move #" + move : "Go to game start";
[tuture-add] return (
[tuture-add] <li>
[tuture-add] <button onClick={() => this.jumpTo(move)}>{desc}</button>
[tuture-add] </li>
[tuture-add] );
[tuture-add] });
[tuture-add]
let status;
if (winner) {
status = "Winner: " + winner;
// ...
</div>
<div className="game-info">
<div>{status}</div>
[tuture-del] <ol>{/* TODO */}</ol>
[tuture-add] <ol>{moves}</ol>
</div>
</div>
);
// ...

查看此时全部项目代码

对于井字棋游戏历史里的每一步移动,我们都创建了一个包含 <button> 的列表元素包含 <li> 。Button 有一个 onClick 处理函数来调用 this.jumpTo()。我们目前还没有实现 jumpTo() 方法。现在我们应该可以看到游戏板旁边出现了一列移动记录,并且在我们的开发者工具控制台里面会显示下面的警告:

Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.

让我们来讨论一下上面的警告的含义。

加上 key

当我们渲染一个列表时,React 会保存一些关于渲染列表元素的信息。当我们更新列表时,React 需要判断哪些元素改变了。我们可能添加、删除、重排列或者更新了列表元素。

想象一下下面的改变,从:

<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>

到:

<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>

除了更新计数,一个人来看上面的改变可能会说:“我们交换了 Alexa 和 Ben 的顺序,然后再 Alexa 和 Ben 中间插入了 Claudia。然而,React 是一个计算机程序,它无法理解我们的意思。因为 React 不知道我们的目的,所以我们需要给每个元素列表指定一个 key 属性来区分它的邻近元素。一种选项是使用字符串 alexabenclaudia。如果我们展示来自数据库的数据,Alexa,Ben 和 Claudia 的数据库 ID 可以作为 key 使用:

<li key={user.id}>{user.name}: {user.taskCount} tasks left</li>

当一个列表重新渲染的时候,React 通过查看每个元素的 key ,并且搜索先前列表查看是否有对应的匹配元素。如果目前的列表存在一个之前没有的 key,React 会创建一个组件。如果当前列表相比先前列表丢失了一个 key,React 会销毁之前的组件。如果两个 key 相匹配,React 会把之前的组件直接挪过来用。Keys 告诉 React 每个组件的身份,这使得 React 可以在重新渲染时维护状态。如果组件的 key 变了,那么这个组件会被销毁,然后使用新的 state 来重新创建这个组件。

key 在 React 中是一个特殊的保留属性(就像 ref 一样 – 一个更加高级的特性)。当一个元素被创建后,React 会提取出它的 key 属性,然后将这个 key 直接保存在返回的元素里面。即使 key 看上去属于 propskey 不能够使用 this.props.key 来引用。React 自动的使用 key 来决定组件是否更新。我们无法获取组件的 key 值。

无论你何时构建一个动态列表组件,我们都强烈推荐你给列表元素赋予 **key** 属性。 如果你的数据没有合适的 key ,你可以要考虑重构你是数据来确保它有。

如果没有指定 key ,React 将在控制台打印一条警告,然后默认使用数组的索引作为 key 。使用数据的所有作为 key 有时候会产生问题,比如当你尝试对列表重排序或者插入或删除列表元素时。显式的传递 key={i} 会使得警告被去掉,但是和默认使用索引一样会产生问题,并且在绝大多数情况下是不被推荐的。

key 不需要全局唯一;它们只需要在组件和它们的兄弟节点间唯一。

在井字棋游戏历史记录中,每个过去的移动步骤都有一个唯一的 ID 与之关联:那就是这个移动步骤的顺序数字。因为这些移动步骤不会重排列,被删除或者在中间新插入步骤,所以使用移动步骤列表的所有是安全的。

在 Game 组件的 render 方法中。我们给 <li> 元素加上 key ,然后 React 的关于 key 的警告应该就消失了:

src/index.js查看完整代码
// ...
const moves = history.map((step, move) => {
const desc = move ? "Go to move #" + move : "Go to game start";
return (
[tuture-del] <li>
[tuture-add] <li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
// ...

查看此时所有项目代码

实现时间旅行

当你点击任何一个列表元素按钮会触发错误,因为 jumpTo 方法还没有定义。在我们实现 jumpTo 方法之前,我们将给 Game 组件的 state 加上 stepNumber 来标志目前我们所看到的步骤。

首先,在 Game 的 constructor 的初始 state 里面加上 stepNumber: 0

class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}

接着,我们将在 Game 里面定义 jumpTo 方法来更新 stepNumber 。并且当 stepNumber 是偶数时,我们将把 xIsNext 设置为 true。

handleClick(i) {
// this method has not changed
}

jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}

render() {
// this method has not changed
}

我们将对 Game 的 handleClick 做一点修改 – 就是那个当你点击 Square 会触发调用的方法。

我们现在加入了 stepNumber state 来表示当前展示给用户的步骤。当我们进行了一步新的移动时,我们需要在 this.setState 语句中加入 stepNumber: history.length 来更新 stepNumber 。这确保在移动了新的一步的时候不会错误的显示相同的移动步骤。

我们还需要将之前读取 this.state.history 更新成读取 this.state.history.slice(0, this.state.stepNumber + 1)。这确保我们 ”回到过去“ 时,再从这个点进行新的移动,我们需要丢掉过去从这个点开始的 “未来” 的历史记录,因为它们现在已经变得不在正确了。

handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}

最终,我们将修改 Game 组件的 render 方法,从之前总是渲染最后一个步骤变成根据 stepNumber 渲染目前选中的步骤。

render() {
const history = this.state.history;
const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);

// the rest has not changed

如果我们点击游戏历史记录的任一步骤,这个井字棋游戏板就会立即更新,去展示当时这个步骤的样子。

保存上面的修改,你的代码将是这个样子:

src/index.js查看完整代码
// ...
squares: Array(9).fill(null)
}
],
[tuture-add] stepNumber: 0,
xIsNext: true
};
}

handleClick(i) {
[tuture-del] const history = this.state.history;
[tuture-add] const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();

// ...
squares: squares
}
]),
[tuture-add] stepNumber: history.length,
xIsNext: !this.state.xIsNext
});
}

[tuture-add] jumpTo(step) {
[tuture-add] this.setState({
[tuture-add] stepNumber: step,
[tuture-add] xIsNext: step % 2 === 0
[tuture-add] });
[tuture-add] }
[tuture-add]
render() {
const history = this.state.history;
[tuture-del] const current = history[history.length - 1];
[tuture-add] const current = history[this.state.stepNumber];
const winner = calculateWinner(current.squares);

const moves = history.map((step, move) => {
// ...

查看此时全部项目代码

总结一下

恭喜你!你已经创建了一个井字棋游戏:

  • 可以让你看井字棋游戏,
  • 在选手获得胜利时会给予显示,
  • 将游戏的历史记录保存为游戏进度,
  • 允许玩家回顾游戏的历史记录以及跳转到之前的游戏步骤。

做的好!我们期望你现在已经对 React 有一个很深刻的理解了。

点击这个链接查看最终的结果:最终结果。

图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2019/11/13/3697248/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!