接下来是最后一个练习,我们将实现“回到过去”的功能,从而在游戏里跳回到历史步骤。
保存历史记录
如果我们直接修改了 square 数组,实现时间旅行就会变得很棘手了。
不过,我们可以使用 slice() 函数为每一步创建 squares 数组的副本,同时把这个数组当作不可变对象。这样我们就可以把所有 squares 数组的历史版本都保存下来了,然后可以在历史的步骤中随意跳转。
我们把历史的 squares 数组保存在另一个名为 history 的数组中。history 数组保存了从第一步到最后一步的所有的棋盘状态。history 数组的结构如下所示:
history = [
// 第一步之前
{
squares: [
null, null, null,
null, null, null,
null, null, null,
]
},
// 第一步之后
{
squares: [
null, null, null,
null, 'X', null,
null, null, null,
]
},
// 第二步之后
{
squares: [
null, null, null,
null, 'X', null,
null, null, 'O',
]
},
// ...
]
再次提升状态
我们希望顶层 Game 组件展示出一个历史步骤的列表。这个功能需要访问 history 的数据,因此我们把 history 这个 state 放在顶层 Game 组件中。
我们把 history state 放在了 Game 组件中,这样就可以从它的子组件 Board 里面删除掉 square 中的 state。正如我们把 Square 组件的状态提升到 Board 组件一样,现在我们来把 state 从 Board 组件提升到顶层的 Game 组件里。这样,Game 组件就拥有了对 Board 组件数据的完全控制权,除此之外,还可以让 Game 组件控制 Board 组件,并根据 history 渲染历史步骤。
首先,我们在 Game 组件的构造函数中初始化 state:
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 组件中接收 squares 和 onClick 这两个 props。因为当前在 Board 组件中已经有一个对 Square 点击事件的监听函数了,所以我们需要把每一个 Square 的对应位置传递给 onClick 监听函数,这样监听函数就知道具体哪一个 Square 被点击了。以下是修改后的结果:
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(