储存历史步骤

我们来实现这样的功能:通过重新访问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组件仅仅有renderSquarerender就可以了;状态初始化和点击事件处理器就都放到Game组件去了。

查看最新的代码

results matching ""

    No results matching ""