储存历史步骤
我们来实现这样的功能:通过重新访问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组件去了。