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 組件傳遞了兩個屬性:value
和onClick
,後者是 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
函數。主要過程如下:
- built-in DOM
<button>
component 中的onClick
屬性通知React設置一個點擊事件監聽器; - 當按鈕被點擊,React 將會呼叫在 Square 組件中
render()
方法裏定義的onClick
事件處理器; - 該事件處理器呼叫
this.props.onClick()
。Square 組件的 props 由 Board 組件規定; - Board 組件將
onClick={() => this.handleClick(i)}
傳給了 Square 組件,所以,當被呼叫時,Board 組件中運行this.handleClick(i)
; - 我們目前還沒有在 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
,而不是如我們所想的把它傳下來。