前端框架 使用React 开发一个井字棋(4) React 保存历史记录

接下来是最后一个练习,我们将实现“回到过去”的功能,从而在游戏里跳回到历史步骤。

保存历史记录

如果我们直接修改了 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(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 函数,使用最新一次历史记录来确定并展示游戏的状态:

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 组件渲染了游戏的状态,因此我们可以将 Board 组件 render 方法中对应的代码移除。修改之后,Board 组件的 render 函数如下所示:

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 组件中。同时,我们也需要修改一下 handleClick 方法,因为这两个组件的 state 在结构上有所不同。在 Game 组件的 handleClick 方法中,我们需要把新的历史记录拼接到 history 上。

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,
    });
}
展示历史步骤记录

在 JavaScript 中,数组拥有 map() 方法,该方法通常用于把某数组映射为另一个数组,例如:

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

我们可以通过使用 map 方法,把历史步骤映射为代表按钮的 React 元素,然后可以展示出一个按钮的列表,点击这些按钮,可以“跳转”到对应的历史步骤。

现在,我们在 Game 组件的 render 方法中调用 history 的 map 方法:

render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ?
        'Go to move #' + move :
        'Go to game start';
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    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>{moves}</ol>
        </div>
      </div>
    );
}

对于井字棋历史记录的每一步,我们都创建出了一个包含按钮 元素的

  • 的列表。这些按钮拥有一个 onClick 事件处理函数,在这个函数里调用了 this.jumpTo() 方法。但是我们还没有实现 jumpTo() 方法。到目前为止,我们可以看到一个游戏历史步骤的列表,以及开发者工具控制台的警告信息,警告信息如下:
  • Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.

    选择一个 key
    const moves = history.map((step, move) => {
          const desc = move ?
            'Go to move #' + move :
            'Go to game start';
          return (
            <li key={move}>
              <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
          );
      });
    

    为 jumpTo 还未定义,所以你点击列表项的按钮时,会出现报错。在我们实现 jumpTo 之前,我们向 Game 组件的 state 中添加 stepNumber,这个值代表我们当前正在查看哪一项历史记录。

    首先,我们在 Game 的构造函数 constructor 中向初始 state 中添加 stepNumber: 0:

    lass Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [{
            squares: Array(9).fill(null),
          }],
          stepNumber: 0,
          xIsNext: true,
        };
      }
    

    然后,我们在 Game 组件中定义 jumpTo 方法以更新状态 stepNumber。除此之外,当状态 stepNumber 是偶数时,我们还要把 xIsNext 设为 true:

    handleClick(i) {
        // 这个方法无更改
      }
    
      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: (step % 2) === 0,
        });
      }
    
      render() {
        // 这个方法无更改
      }
    

    接下来,我们还要修改 Game 组件的 handleClick 方法,当你点击方格的时候触发该方法。

    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.xIsNext ? 'X' : 'O';
        this.setState({
          history: history.concat([{
            squares: squares
          }]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext,
        });
      }
    

    最后,修改 Game 组件的 render 方法,将代码从始终根据最后一次移动渲染修改为根据当前 stepNumber 渲染。

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);
         // 其他部分没有改变
    

    完整代码:

    function Square(props) {
      return (
        <button className="square" onClick={props.onClick}>
          {props.value}
        </button>
      );
    }
    
    class Board extends React.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>
        );
      }
    }
    
    class Game extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [
            {
              squares: Array(9).fill(null)
            }
          ],
          stepNumber: 0,
          xIsNext: 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.xIsNext ? "X" : "O";
        this.setState({
          history: history.concat([
            {
              squares: squares
            }
          ]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext
        });
      }
    
      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: (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 desc = move ?
            'Go to move #' + move :
            'Go to game start';
          return (
            <li key={move}>
              <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
          );
        });
    
        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>{moves}</ol>
            </div>
          </div>
        );
      }
    }
    
    // ========================================
    
    ReactDOM.render(<Game />, document.getElementById("root"));
    
    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]
      ];
      for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
          return squares[a];
        }
      }
      return null;
    }
    
    
    发布了181 篇原创文章 · 获赞 14 · 访问量 4万+

    猜你喜欢

    转载自blog.csdn.net/LU_ZHAO/article/details/105003095