到目前为止,demo 的操作是不能回退的,点击格子以后,想要记录历史的操作,就需要 使用 slice() 函数为每一步创建 squares 数组的副本,同时把这个数组当作不可变对象。这样就可以把所有 squares 数组的历史版本都保存下来了,然后也可以在历史的步骤中随意跳转。
由于在 Game 组件要展示一个历史步骤的列表,这个功能需要访问 history 的数据,因此需要把 state 放在顶层 Game 组件中;而要保存历史数据,则需要在 handleClick 函数中做处理,需要在 Game 组件中实现 handleClick 方法。
接下来,删除 Board 中的 state 以及 handleClick 处理函数,修改 Board 组件:
import React, { Component } from 'react'
import Square from './Square'
class Board extends Component {
renderSquare (i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)} />
)
}
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>
)
}
}
export default Board
修改 Game.js 文件,在 render 中通过 calculateWinner 计算结果来展示当前游戏状态:
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.isNext? 'X' : 'O');
}
在 return 中添加div来展示状态:
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>
);
修改 handleClick 方法:
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.isNext? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
isNext: !this.state.isNext
})
}
修改 Game 组件的 render 方法:
render () {
const history = this.state.history
const current = history[history.length - 1]
const winner = calculateWinner(current.squares)
const moves = history.map((step, move) => {
const text = move ? 'Go to move:' + move : 'Go to game start'
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{text}</button>
</li>
)
})
let status
if (winner) {
status = 'winner:' + winner
} else {
status = 'Next player:' + (this.state.isNext ? 'X' : 'O')
}
return (
<div className="game-container">
<div >
<div>{status}</div>
<ol className="step-item">{moves}</ol>
</div>
<div className="game">
<div className="game-board">
<Board squares={current.squares}
onClick={(i) => this.handleClick(i)} />
</div>
</div>
</div>
)
}
}
这里通过一个 包含按钮 元素的 li 的列表来展示历史步骤。
可以看到,上面的 li 标签,设置了 key 值。这是因为:
每当一个列表重新渲染时,React 会根据每一项列表元素的 key 来检索上一次渲染时与每个 key 所匹配的列表项。如果 React 发现当前的列表有一个之前不存在的 key,那么就会创建出一个新的组件。如果 React 发现和之前对比少了一个 key,那么就会销毁之前对应的组件。如果一个组件的 key 发生了变化,这个组件会被销毁,然后使用新的 state 重新创建一份。
key 是 React 中一个特殊的保留属性。当 React 元素被创建出来的时候,React 会提取出 key 属性,然后把 key 直接存储在返回的元素上。虽然 key 看起来好像是 props 中的一个,但是你不能通过 this.props.key 来获取 key。React 会通过 key 来自动判断哪些组件需要更新。组件是不能访问到它的 key 的。
所以,只要构建动态列表的时候,都要指定一个合适的 key。
在 state 中添加 stepNumber代表当前正在查看的那一项历史记录。
添加 jumpTo 方法,每次点击历史记录中的某一条时会触发 jumpTo 方法,更新 stepNumber 的值。
jumpTo(step) {
this.setState({
stepNumber: step,
isNext: (step % 2) === 0,
});
}
修改 Game 组件的 handleClick 方法,每次点击格子时调用 setState 方法更新 stepNumber 。
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.isNext ? 'X' : 'O'
this.setState(
{
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
isNext: !this.state.isNext
}
)
console.log('history', this.state.history)
console.log('current', current)
}
打印历史数据:
第一次点击格子:
第二次点击格子:
如上面两图所示,在 handleClick 中打印 history 历史数据,可以看到每一步的操作都被记录了下来,保存在 history 中了。
最后,修改 Geme 组件的 render 方法,根据当前 stepNumber 来渲染。
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares)
这样,点击任意一步,棋盘都会渲染那一步的棋子样式。
现在,棋盘可以记录每一步的操作,并可随意回退到之前的操作,如下图所示:
完整代码如下:
import React, { Component } from 'react'
import '../assets/css/Game.css'
import Board from './Board'
class Game extends Component {
constructor(props) {
super(props)
this.state = {
history: [{
squares: Array(9).fill(null)
}],
stepNumber: 0,
isNext: true
}
}
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.isNext ? 'X' : 'O'
this.setState(
{
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
isNext: !this.state.isNext
}
)
}
jumpTo (step) {
this.setState({
stepNumber: step,
isNext: (step % 2) === 0
})
}
render () {
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares)
const moves = history.map((step, move) => {
const text = move ? 'Go to move:' + move : 'Go to game start'
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{text}</button>
</li>
)
})
let status
if (winner) {
status = 'winner:' + winner
} else {
status = 'Next player:' + (this.state.isNext ? 'X' : 'O')
}
return (
<div className="game-container">
<div >
<div>{status}</div>
<ol className="step-item">{moves}</ol>
</div>
<div className="game">
<div className="game-board">
<Board squares={current.squares}
onClick={(i) => this.handleClick(i)} />
</div>
</div></div>
)
}
}
export default Game
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]
]
const len = lines.length
for (let i = 0; i < len; i++) {
const [a, b, c] = lines[i]
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a]
}
}
return null
}
// 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',
// ]
// },
// // ...
// ]