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