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”.

下篇,我們來談談這條警告是什麼意思。

發佈了49 篇原創文章 · 獲贊 40 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章