React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (3) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (4) <译自官方文档>
4-存储历史步骤
我们来实现这样的功能:通过重新访问 board 旧的状态,穿越回到之前的某一步。目前我们已经做到:每走一步棋,都随即创造一个新的squares
数组。由此,我们可以同步地存储 board 的旧状态。
我们准备在状态中存储这么一个对象:
code
history = [
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// ...
]
我们希望由顶层的 Game 组件来负责显示一个列表,以展示每一步棋的历史。所以,就像之前我们把 Square 中的状态提升到 Board 组件一样,现在我们进一步把状态从 Board 提升到 Game 组件。这样,在顶层就有了我们需要的全部信息。
首先,Game 组件中添加一个constructor
,设置初始状态:
code
class Game extends React.Component {
constructor() {
super();
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 组件,让它通过 props 接收squares
,同时由 Game 组件来规定其onClick
属性,就像之前我们对 Square 组件做的一样。你可以把每个小方格的位置传进点击事件处理器里,这样我们仍然能知道被点击的小方块是哪一个。你需要完成这些步骤:
- 删除 Board 组件中的
constructor
; - 在 Board 组件的
renderSquare
中,把this.state.squares[i]
替换为this.props.squares[i]
; - 在 Board 组件的
renderSquare
中,把this.handleClick(i)
替换为this.props.onClick(i)
。
现在,整个 Board 组件看起来是这样:
code
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
应该显示历史步骤记录,并接管游戏状态(status)的计算:
code
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>
);
}
Game 组件现在渲染了 status,所以我们可以从 Board 组件render
函数中删去<div className="status">{status}</div>
,以及计算status的相关代码:
code
render() {
return (
<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>
);
}
下一步,我们需要把 Board 组件中handleClick
方法的实现移动到 Game 组件。你可以从前者中剪切下来,粘贴到后者。
我们还需要进行一点点改动,因为 Game 组件的状态和前者的相比,构成略有不同。Game 组件的handleClick
能通过连接 (concat) 新的历史入口 (history entry),向栈中添加 (push) 新的 entry。
Game 组件的handleClick
方法通过.concat()
把新的步骤记录加入到数据栈中,由此构成新的新的储存历史步骤的数组。
code
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,
});
}
现在,Board 组件仅仅有renderSquare
和render
就可以了;状态初始化和点击事件处理器就都放到 Game 组件去了。
显示每一步棋
我们把游戏进行到现在所走的每一步棋都展示出来。我们已经知道,React 元素是 JS 的“头等对象”,可以被储存、传送。为了渲染多个 React 的多个条目,我们传入了一个包含 React 元素的数组。构建它最常用的方法就是,对你的数组使用.map
。在 Game 组件的render
方法中,咱们就这么干:
code
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
const moves = history.map((step, move) => {
const desc = move ?
'Move #' + move :
'Game start';
return (
<li>
<a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
</li>
);
});
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>{moves}</ol>
</div>
</div>
);
}
对于历史记录里的每一个步骤,我们都建立一个列表条目<li>
,里面有一个<a>
标签,它不指向任何地址(href="#"
),而是会带有一个点击事件处理器,我们很快就会实现它。写代码至此,你应该会得到一个列表,记录着游戏中的历史步骤,还有一行警告:
Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.
下篇,我们来谈谈这条警告是什么意思。