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 組件僅僅有renderSquare
和render
就可以了;狀態初始化和點擊事件處理器就都放到 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”.
下篇,我們來談談這條警告是什麼意思。