[官網學 React] Tutorial:#3 Completing the Game 完成遊戲

Juju 的自學筆記
9 min readApr 9, 2021

--

網址(英文):https://reactjs.org/tutorial/tutorial.html#completing-the-game
網址(中文):https://zh-hant.reactjs.org/tutorial/tutorial.html#completing-the-game

上一篇我們學了如何傳遞和使用 React 元件內的資料:[官網學 React] Tutorial:#2 Overview 概論

但我們在點擊格子時,只會出現 X,這篇我們就會學到如何交替切換成 O,並判斷誰是贏家。

這篇要來將 OOXX 井字遊戲完成囉!

提升狀態(State)

將元件的 state 資料傳遞給父層,就是提升 state。

為了能判斷誰是贏家,我們需要掌握九個格子中的 value 是什麼。

首先想到一種方法是用父層 Board 去詢問所有子元件,雖然這方法在 React 是可行的,但官網說他們不鼓勵這樣做。
因為程式碼會變得不好理解和容易出錯,而且較難重構(refactor)。

最好的方法是將遊戲狀態存在父層 Board 元件。
Board 元件可以透過傳遞 prop 讓所有 Square 子元件知道自己要呈現的是什麼,空的或 O 或 X。

想要收集多個子元件的資料、或是讓子元件兩兩溝通,必須在父層宣告共用的 state。父層可以透過 props 將共用的 state 下傳給子元件。這樣能讓子元件們保持彼此和父層的同步。

好的,讓我們接著來實作在井字遊戲裡!

首先在父層 Board 加入一個建構子(constructor),並預設 Board 的 state 為 9 個 null 的陣列。

從上圖看到原本 Board 的 renderSquare() 回傳的是變數 i 的值,那是我們在前一篇學習 透過 Props 傳遞資料 時設定的。

現在我們要將它的 value 值改成用 Board 的 state 陣列裡的值,如以下:

再來,因為是父層 Board 在控管每一個子元件 Square 的格子內會呈現什麼(O、X 或 空),我們需要建立一種可以讓子元件 Square 被點擊後能更新父層 Board 的 state 的方法。

由於 state 是每個元件自己獨立使用的,所以子元件沒辦法直接更改到父層的 state。

這邊的做法是在父層傳一個函式 handleClick() 給子元件 Square,當子元件 Square 被點擊時,就會呼叫這個函式:

在父層傳一個函式 handleClick() 給子元件 Square

修改子元件 Square:

  1. 刪除建構子,不需要了
  2. 修改原本 onClick 會驅動的動作,由 this.setState() 替換成 this.props.onClick()
  3. 修改呈現的結果由 this.state.value 替換成 this.props.value

這樣當使用者點擊格子時,就會呼叫父層的函式 handleClick() 。

不過目前還尚未定義 handleClick() 這個函式,所以會看到目前的畫面呈現錯誤。

整個點擊事件的運作過程如下:

  1. 在內建的 DOM <button> 元件的 onClick prop 告訴 React 要設定一個 click event listener(監聽點擊事件)
  2. 當按鈕被點擊時,React 會呼叫 Square 定義的 onClick event handler(onClick 等號後花括內的內容 {this.props.onClick()} 被呼叫到)
  3. Square 的 onClick prop 是由父層 Board 指定的
  4. 因為父層 Board 傳函式給子元件 Square,所以當 Square 被點擊時,透過 onClick = {this.props.onClick()} 就能呼叫父層的函式 handleClick()

onClick 屬性對 React 來說有特別的意義,因為它是一個 React 內建的元件。客製化的元件(例如 Square),命名方式則可依個人喜好。在 React 的傳統,是用 on[Event] 為 event 的 prop 命名,而 handle[Event] 則是為handle event 的方法命名。

在父層 Board 定義函式 handleClick() 吧!

現在我們又可以正常點擊格子,看到出現 X 了。

差別在,現在 state 是存在父層元件而非各個子元件了。
當父層的 state 一有變動,子元件就會自動重新渲染。

將這些子元件的 state 都存在父層 Board,將能用來決定勝負。

在 React 的詞彙中,Square component 現在是被控制的元件(controlled components),父層 Board 對子元件們有完全的掌握。

為何 Immutability(不可變性)是重要的

這部分在探討,為何我們在函示 handleClick() 裡要用 .slice() 的方式複製一個陣列給 const squares?
修改在複製品、保留原本的資料,有什麼好處嗎?

用一組範例來看。

第一種方法:直接修改原始資料的值

直接修改原始資料的值

第二種方法:複製一份跟原始資料相同的資料,修改在複製出來的資料上,原始資料保持原樣。

修改在複製出來的資料上,原始資料保持原樣

兩種方法的結果是一樣的,而沒有直接修改在原始資料的好處是:

  • 簡化複雜的功能
    Immutability(不可變性)讓複雜的功能變得更易於實作。
    後面我們實作的 time travel 這個功能,能讓我們檢視井字遊戲的遊戲過程,還可以復原步驟回到上一部。
    而這功能並非專用於遊戲,「復原」和「重做」功能在應用程式裡是很尋常的需求。
    避免直接修改原始資料,讓我們能留住之前的版本,在之後能再次使用它們。
  • 偵測改變
    直接修改原始資料就很難偵測改變,因為已經直接改掉了。
    偵測改變需要比對複製後的新資料和原本的資料,並走訪整個物件樹(object tree)。
    相較之下,在第二種方法的 object 中偵測改變就容易多了,如果某個 object 和之前不一樣,就知道這個 object 已經被改變了。
  • 在 React 中決定何時重新 render
    第二種方法的主要好處就是幫助你在 React 建立一個單純的元件。
    因為有複製出一個和保留一個不可變的資料,可以容易的偵測資料是否有發生改變,這就能幫助判斷何時要重新渲染元件。

可以在 Optimizing Performance 了解更多有關 shouldComponentUpdate() 的知識和如何建出單純的(pure)元件的方法。

我應該是會等讀到 Docs 的時候再去細讀。

函式元件(Function Component)

現在我們要試著將 Square 從類別元件改成函式元件。

在 React,函式元件是一個比較簡單的創建元件方式,這寫法內只包含一個 render,沒有屬於自己的 state。

比起之前我們用 class 的方法,需要 extends React.Component,用函式寫的元件可以將 props 當作函式的輸入參數,回傳要渲染什麼。

函式寫法比 class 寫法不冗長乏味,許多元件都能以這種形式表達。

那我們來試試用函式寫法改寫 Square 元件:

上半部是 class 寫法,下半部是函式寫法

輪流(Taking Turns)

到目前為止我們在點擊格子時都只會出現 X,終於要來做能切換 O 的功能啦!

將第一步預設為 X,可以在 state 設定預設值:

接著我們將 O 或 X 的結果存到 squares,再到 handleClick() 用this.setState翻轉 xIsNext 這布林值:

這時我們就可以在點擊格子時交替出現 X 和 O 了!

不過九宮格上面的 Next Player 目前仍固定是 X,現在我們也幫它改成有交替的效果:

宣布獲勝者

我們已經可以呈現出下一個玩家是誰,現在我們要在遊戲出現贏家時呈現出結果和停止遊戲。

首先在程式的最下方貼上以下程式碼:

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

這個函式會確認贏家,並回傳 X、O 或 null。

我們在 Board 的 render() 呼叫 calculateWinner(squares) 這個函式,確認是否有贏家出現了。

如果有贏家出爐,我們就在畫面秀出「Winner: X」或「Winner: O」。

我們現在不但可以切換玩家,還可以宣告最後的贏家。

但是!

還有一些需要優化的地方,就是被選過的格子依然能被重複點擊和更改,而且已經出現贏家後也還能再更改。
這不符合遊戲規則,所以我們需要在 handleClick() 做一些優化:

有贏家時或格子已經有值時,直接 return,不執行接下來的動作

很好!

恭喜恭喜~ 完成了一個井字遊戲囉🎉🎉

也已經學會了基礎的 React,下一篇我們將進入更進階的主題:Adding Time Travel 加上時間旅行

--

--

No responses yet