React 实现井字棋游戏 (tic-tac-toe) 教程 (5) <译自官方文档>

React 实现井字棋游戏 (tic-tac-toe) 教程 (1) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (2) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (3) <译自官方文档>
React 实现井字棋游戏 (tic-tac-toe) 教程 (4) <译自官方文档>

4-存储历史步骤

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

查看最新的代码

显示每一步棋

我们把游戏进行到现在所走的每一步棋都展示出来。我们已经知道,React 元素是 JS 的“头等对象”,可以被储存、传送。为了渲染多个 React 的多个条目,我们传入了一个包含 React 元素的数组。构建它最常用的方法就是,对你的数组使用.map。在 Game 组件的render方法中,咱们就这么干:

code

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 ?
        'Move #' + move :
        'Game start';
      return (
        <li>
          <a href="#" onClick={() => this.jumpTo(move)}>{desc}</a>
        </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>
    );
  }

查看最新的代码

对于历史记录里的每一个步骤,我们都建立一个列表条目<li>,里面有一个<a>标签,它不指向任何地址(href="#"),而是会带有一个点击事件处理器,我们很快就会实现它。写代码至此,你应该会得到一个列表,记录着游戏中的历史步骤,还有一行警告:

Warning: Each child in an array or iterator should have a unique “key” prop. Check the render method of “Game”.

下篇,我们来谈谈这条警告是什么意思。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值