React 實現井字棋遊戲 (tic-tac-toe) 教程 (3)

React 實現井字棋遊戲 (tic-tac-toe) 教程 (1) <譯自官方文檔>
React 實現井字棋遊戲 (tic-tac-toe) 教程 (2) <譯自官方文檔>

3-狀態提升

至此,我們已經擁有了編寫井字棋遊戲的基本構件。但現在,狀態(state)是被包裹在各個 Square 組件內的。爲了完成這個遊戲,我們還需要做這兩件事:檢查是否已經有玩家勝出;以及在小方格中輪流填入“X”和“O”。爲了檢查是否已經有玩家獲勝,我們需要把9個小方格的狀態值都集中到一個地方,而不是讓它們分散在各個 Square 組件內部。

你可能會想到,讓 Board 組件去查詢各個 Square 組件的當前狀態值。當然,單純從技術上講,用 React 是能做到這個的,但我們並不鼓勵這麼幹。因爲這會讓代碼變得不易理解,更脆弱,也更難重構。

所以,最佳的方案,是把狀態值都存儲到 Board 組件,而非各個 Square 組件中。這樣,Board 組件就可以告訴各個 Square 組件應該顯示什麼。這就跟之前,我們讓每個小方格顯示各自序號所用的方法是一樣的。

當你需要從多個子組件中聚集數據,或者想讓兩個子組件互相通信的時候,你應該把狀態提升到父組件之中。父組件可以通過props把狀態值傳回其子組件。如此,子組件互相之間、子組件和父組件之間都能保持同步。

在重構 React 組件時,像這樣提升狀態的做法是非常常見的。藉着這次機會,我們也來試一下。在 Board 組件中,添加 constructor 函數,並設置初始狀態:一個包含9個 null 的數組,它們分別對應9個小方格。

code

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  renderSquare(i) {
    return <Square value={i} />;
  }

  render() {
    const status = 'Next player: X';

    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>
    );
  }
}

待會兒,我們會填入一些東西,讓它變成類似這樣:

code

[
  'O', null, 'X',
  'X', 'X', 'O',
  'O', null, null,
]

現在,Board 組件的renderSquare方法是這樣子的:

code

 renderSquare(i) {
    return <Square value={i} />;
  }

修改它,把value屬性傳給 Square 組件:

code

 renderSquare(i) {
    return <Square value={this.state.squares[i]} />;
  }

查看最新的代碼

現在,我們來改變小方塊被點擊後的行爲。Board 組件存儲着填小方塊的東西,這意味着我們需要想辦法讓 Square 組件更新 Board 的狀態。因爲狀態是組件私有的,所以我們不能直接從 Square 組件修改 Board 組件的狀態。

通常的方法是這樣的:從 Board 組件向 Square 組件傳一個函數,讓它在小方塊被點擊時執行。再次修改 Board 組件中的renderSquare方法,讓它變成這樣:

code

renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

爲了提高可讀性, 我們把這個被返回的元素分開寫成多行。再用括號括住它.這樣能防止 JavaScript 在 return 後面加個分號而打斷代碼語句。

現在,我們從 Board 組件向 Square 組件傳遞了兩個屬性:valueonClick,後者是 Square 組件可以呼叫的函數。我們繼續對 Square 組件做如下改動:

  • 把 Square 組件的render函數中的this.state.value替換爲this.props.value;
  • 把 Square 組件的render函數中的this.setState()替換爲this.props.onClick();
  • 從 Square 組件中刪去constructor函數,因爲它已經不包含任何狀態了。

做了以上改動後,這個組件成了這樣子:

code

class Square extends React.Component {
  render() {
    return (
      <button className="square" onClick={() => this.props.onClick()}>
        {this.props.value}
      </button>
    );
  }
}

現在,當小方格被點擊時,會呼叫從 Board 組件傳來的onClick函數。主要過程如下:

  1. built-in DOM <button> component 中的onClick屬性通知React設置一個點擊事件監聽器;
  2. 當按鈕被點擊,React 將會呼叫在 Square 組件中render()方法裏定義的onClick事件處理器;
  3. 該事件處理器呼叫this.props.onClick()。Square 組件的 props 由 Board 組件規定;
  4. Board 組件將onClick={() => this.handleClick(i)}傳給了 Square 組件,所以,當被呼叫時,Board 組件中運行this.handleClick(i)
  5. 我們目前還沒有在 Board 組件中定義handleClick()方法,所以代碼會出錯。

需要注意的是, DOM <button>組件中的onClick對 React 有着特別的意義。我們本可以把 Square 組件中的onClick和 Board 組件中的handleClick叫成別的名字。然而,React app 中有約定俗成的方式:對於處理器屬性用on*的格式命名;對於具體實現,則用handle*的格式命名。

請試着點擊小方格。應該會收到報錯信息,因爲我們還沒有定義handleClick。現在,把它加到 Board 組件的類中。

code

class Board extends React.Component {
  constructor() {
    super();
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    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>
    );
  }
}

查看最新的代碼

我們用.slice()來拷貝一份squares數組的副本,再對副本進行操作。不要直接改變原數組。查看這一部分來了解不可改變性( immutability)的重要性。

如果現在再點擊小方格,格子裏應該又會出現“X”了。但此時,狀態值是存儲在 Board 組件裏的,而不是像之前,存在各個 Square 組件中。這讓我們的遊戲編寫工作得以繼續進行。注意,無論 Board 組件中的狀態值何時改變,Square 組件總能自動重新渲染。

Square 組件不再保有自己的狀態,而是改爲從父組件,即 Board 組件那裏接收;同時,當它被點擊的時候,會通知其父組件。我們把這種組件叫做受控組件

爲什麼不可變性很重要

在之前的示例代碼中,我們建議使用.slice()運算符來拷貝一份squares數組,再在其副本上進行數據改動,以防止原有的數組被修改。現在,我們來具體談談它的內涵,和這麼做的重要性。

通常來說,修改數據的方法有兩種。第一種方法是通過直接改動變量的值來修改(mutate)原有數據,第二種方法是使用一份改動後的副本,以此替換(replace)原有數據。

改動(mutate)原數據

code

var player = {score: 1, name: 'Jeff'};
player.score = 2;
// 現在 player 是 {score: 2, name: 'Jeff'}
不改動(mutate)原數據

code

var player = {score: 1, name: 'Jeff'};

var newPlayer = Object.assign({}, player, {score: 2});
// 現在 player 沒有改變, 而 newPlayer 是 {score: 2, name: 'Jeff'}

// 或者使用對象展開符:
// var newPlayer = {...player, score: 2};

最終的結果是一樣的。但是,不直接改動基礎數據的方法卻能帶來一些額外的好處:它有助於提升組件或者整個應用的性能。

更簡單的 撤銷/重做 和 穿越功能

不可改變性 也能讓一些複雜的特性實現起來更容易。例如,在本教程後期,我們將要實現在棋局的不同階段間穿越的功能。避免數據的變動(mutation),能讓我們保持對舊版本數據的引用。如果我們需要的話,就能在它們之間切換。

追蹤變動

對於被直接改動(mutate)的對象,我們難以判斷它們是否被修改,因爲所以改動都直接在原對象上進行的。這要求比較當前對象和之前的拷貝的副本,遍歷整個對象數,比較每個變量與值。這個過程可能會變得越來越複雜。

而判斷不可變對象是否被改動則是相當容易的。 如果被引用的對象與之前的不同,則對象已更改。就這麼簡單。

在React中 確定何時重新渲染

在React中,當建立純組件時,不可改變性帶來的好處最明顯。對於不可改變的數據,我們能很容易地確認改動是否發生,藉此,我們就可以確定組件何時要求被重新渲染。

想要了解shouldComponentUpdate()以及如何構建純組件,請查看優化性能

函數式聲明組件

我們已經移除了 Square 組件的 constructo r函數。事實上,對於 Square 這樣,僅僅由render方法構成的組件,React 有一種更簡單的聲明組件的語法,叫函數式聲明組件。不必用extends React.Component來定義組件,你僅僅只需寫一個函數,它接受屬性,返回需要被渲染的東西即可。

用下面這個函數替換掉整個 Square 的類:

code

function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

你需要把兩個this.props都換成props。你的 app 中,很多組件都能寫成函數式聲明的組件。這樣的組件更容易寫,而且 React 以後也會繼續優化它們。

整理代碼時,我們把onClick={() => props.onClick()}也換成onClick={props.onClick}。因爲對於本案例來說,把函數傳下來就已經足夠了。注意,寫成onClick={props.onClick()}是不行的,因爲它會立即調用props.onClick,而不是如我們所想的把它傳下來。

查看最新的代碼

更新

React 實現井字棋遊戲 (tic-tac-toe) 教程 (4) <譯自官方文檔>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章