[官網學 React] Tutorial:#4 Adding Time Travel 加上時間旅行
網址(英文):https://reactjs.org/tutorial/tutorial.html#adding-time-travel
網址(中文):https://zh-hant.reactjs.org/tutorial/tutorial.html#adding-time-travel
目前我們已經 完成遊戲 了,上一篇:[官網學 React] Tutorial:#3 Completing the Game 完成遊戲。
來到 Tutorial 的最後一個練習,我們來讓「時光倒流」,回到上一步變得可能吧!
儲存歷史動作
在上一篇我們探討過 Immutability(不可變性)的好處,並且在每一個點擊事件都用 slice()
複製產生一個新的 squares 陣列,並用 const 宣告這組陣列讓它是不可變的常數。
這麼做讓我們可以保存過往 squares 陣列的每一個版本,能遊歷於每個已發生的事件之間。
接下來我們打算將過往的 squares 陣列儲存在另一個陣列「history」,這個陣列會紀錄從第一步到最後一步的 state,預期長得像這樣:
再來需要決定的是,該把這個有歷史資訊的 state 放在哪個元件來管控呢?
再次提升 State
因為我們想在最上層的元件 Game 秀出遊戲過程的歷史動作,所以將在 Game 元件存放並讀取 history。
Game 元件是 Board 元件的爸爸,是 Square 元件的阿公。
要將 history 放在 Game 元件,代表需要將 Board 元件內的 squares 資料都搬過來,就跟我們在上一篇將 state 從子元件 Square 搬到父層 Board 的做法雷同,這次是要將 Board 元件的 state 再搬到最上層的 Game 元件。
這讓最上層的 Game 元件對它的子元件 Board 握有完全的掌控,可以讓 Board 渲染出前一步的歷史畫面。
首先移除 Board 的建構子,搬到 Game 元件,squares 物件改成放在 history 陣列中:
接著,我們讓 Board 元件變成從父層 Game 接收 squares 和 onClick 這兩個 props:
- 用
this.props.squares[i]
取代原本的this.state.squares[i]
- 用
this.props.onClick(i)
取代原本的this.handleClick(i)
再來是更新 Game 元件的 render 函式,讓這函式使用到最近期的 history,以判斷和呈現出當前的遊戲狀態:
- 新增
history
和current
常數 - 將
winner
和status
從 Board 元件搬過來 - 傳 squares 和 onClick 兩個 props 給 Board 元件使用與呼叫
- 顯示 status
狀態現在是由 Game 元件來顯示,所以 Board 可以移除顯示狀態的部分了:
最後將 handleClick 函式從 Board 元件搬到 Game 元件,並做一些改動:
- 新增
history
和current
常數 - squares 的值用
current.squares.slice()
取代this.state.squares.slice()
this.setState
變成更新 history 的值
*這邊使用 concat() 而非熟悉的 push(),是因為 concat() 不會修改到原本的陣列。
現在,Board 元件只需要 renderSquare 和 render 兩個函式,遊戲的資料和點擊事件都應該放在 Game 元件內。
顯示歷史動作
因為我們已經有了歷史紀錄,我們能向玩家展示過去有哪些動作。
React 的元素是 first-class JavaScript 物件,可以在應用程序中傳遞這些元素。
在 JavaScript 中,陣列有一個很普遍被使用在映射(mapping)的方法 map() ,用法範例:
使用 map(),我們可以將歷史動作呈現在用按鈕表現的一列 React 元素,將能藉由點擊這些按鈕回到之前的狀態。
開始前我想先用 console.log 看一下 map 裡函式的輸入值是什麼:
再繼續照著官網做下去:
我們建立一列項目 <li>,每個項目包含一個按鈕元素 <button>,按鈕有一個 onClick 用來呼叫 this.jumpTo()
(尚未實作這個功能)。
截至目前,我們可以看到一列歷史動作了,還有紅紅的警告:
Warning: Each child in a list should have a unique “key” prop.
接著我們就要來討論這個警告了。
挑選一個 key
當我們渲染列表時,React 會儲存一些有關列表中每個項目的資訊。
而在更新列表時,React 需要判斷哪些項目有被變更,可能是新增、移除、重新排序、或更新列表的項目,例如:
如果讓一個人類來說明,他可能會說 Alexa 和 Ben 的順序被調換了,並在他們之間加入 Claudia。
但 React 是一個電腦程式,它無法看出我們的意圖。
所以我們需要給列表中每個項目具體指出一個 key 屬性,讓 React 能區別他們彼此。
一種做法是用字串 alexa
、ben
、claudia
。
而如果我們是從資料庫取得這些資料,就可以用資料庫分別給這些項目的 ID 當作 key:
當一個列表重新渲染,React 擷取出每一個項目的 key,並在先前的列表中搜索匹配的 key。
- 如果當前列表中有某個 key 不存在先前的列表中,React 會新增一個帶有這個 key 的元件
- 如果當前的列表中遺失一個在先前列表中有的 key,React 會銷毀那個原本有而現在沒有的元件
- 如果當前列表和先前列表的 key 匹配,則對應的元件被動到(可能修改內容之類)
key 們告訴 React 每一個元件的身份(就像身分證字號),讓 React 能夠在每一次的重新渲染中維護好 state。
如果有一個元件的 key 改變了,這個元件將被銷毀,再新增一個帶有這個 key 和有新 state 的新元件。
key 是 React 中的特殊保留屬性(就像功能較為進階的 ref
)。
新增一個元素(element)時,React 會取出它的 key 屬性,直接將 key 保存在回傳的元素內。
雖然 key 看起來跟 props
像同一類的關鍵字,但 key 並不能利用 this.props.key 參照。
- React 會自動依據 key 來決定哪些元件需要更新
- 元件無法查問自己的 key(無法知道自己的 key 是什麼)
官網強烈建議,創建動態列表時,要指定適當的 key。
如果你的列表沒有 key 的話,可能要考慮重構你的資料,要讓它有 key。
若沒指定 key,React 會秀出警告和預設使用陣列的索引當作 key。
但是使用陣列的索引作為 key,在重新排序列表中的項目或插入 / 移除項目時會有問題。
只要移除一個中間項目,後面所有項目的索引都會因此改變,代表 React 需要刪除後面所有項目,並新增有新的 key 值的這些項目,效能會不好。
而明確地指定 key={i}
雖然能避開警告,卻還是會跟使用 array 的索引一樣產生同樣的問題。在大多數的情況下,官網不建議我們這麼做。
key 不需要在全域範圍內獨一無二,他們只需要在他們的兄弟姐妹元件之間是獨一無二即可。
實作時間旅行
在井字遊戲中,每一動都有一個獨特的 ID,就是他們的序號。
這些步驟永遠不會被重新排序、刪除、或增加在中間,所以用每一動的索引 move 當作 key 是安全的:
在我們定義 jumpTo 函式之前,先新增一個資料 stepNumber
在 Game 元件的 state,它用來指示我們正在看的是哪一步:
定義好 stepNumber
後,就可以接著來定義 jumpTo 函式了。
jumpTo 函式用來更新 stepNumber
,並且當 stepNumber
是偶數時,我們將 xIsNext
設為 true:
接著我們要更新 Game 元件的 handleClick 函式,這個函式是在使用者點擊格子時會被呼叫到。
- 常數 history 透過 slice() 的方法複製出第一步到這一步的 history,確保如果我們之後點擊回這一動,會從這裡開始,而比這步更後面的步驟都會被移除。
- stepNumber 顯示的是使用這現在到哪一步,所以當使用者點擊格子時,就需要更新 stepNumber,才不會一直卡在同一步。
最後,要來更新 Game 元件的 return(),讓畫面會依據使用者的選擇而改變。
現在當我們點擊遊戲的歷史步驟,就會看到遊戲狀態更新回點擊步驟的狀態。
總結
恭喜我們完成井字遊戲了 🎉🎉🎉
現在這版的功能有:
- 可以玩井字遊戲
- 當有玩家獲勝時會秀出結果
- 隨著遊戲的進行而儲存遊戲狀態
- 讓玩家可以查看遊戲的歷史記錄,並點擊查看之前版本的狀態
延伸挑戰
如果有更多時間或想練習新的 React 技巧,官網有提供一些目前這版井字遊戲可以再改善的項目,以下依照程度逐漸增加困難度排序:
- 在歷史動作列表中,用(欄,列)的格式來顯示每個動作的位置
- 在動作列表中,將目前被選取的項目加粗
- 改寫 Board 元件,使用兩個 loop 建立方格而非用寫死的方法
- 加上一個開關(toggle),可以切換歷史動作列表要順序還是反序
- 當有人獲勝時,強調出那三個連成一直線的格子
- 當無人獲勝時,顯示這場遊戲結果是平局的訊息
結尾
這份 Tutorial 教學指南帶著我們碰觸到 React 的觀念,其中有元素、元件、props 和 state。
更多的詳細說明請至: the rest of the documentation
想學習更多有關定義元件,請至:React.Component
API reference