學(xué)習(xí)(React 官方中文文檔)[https://zh-hans.react.dev/]
教程: 井字棋游戲
本教程將引導(dǎo)你逐步實(shí)現(xiàn)一個簡單的井字棋游戲,并且不需要你對 React 有任何了解。在此過程中你會學(xué)習(xí)到一些編寫 React 程序的基本知識,完全理解它們可以讓你對 React 有比較深入的理解。
PS: 本教程專為喜歡 理論與實(shí)戰(zhàn)相結(jié)合 以及希望快速看見成果的人而設(shè)計(jì)。如果你喜歡逐步學(xué)習(xí)每個概念,請從 描述 UI 開始。
實(shí)現(xiàn)的是什么程序?
本教程將使用 React 實(shí)現(xiàn)一個交互式的井字棋游戲。
你可以在下面預(yù)覽最終成果:
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
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;
}

概覽

App.js:
App.js 的代碼創(chuàng)建了一個 組件。在 React 中,組件是一段可重用代碼,它通常作為 UI 界面的一部分。組件用于渲染、管理和更新應(yīng)用中的 UI 元素。讓我們逐行查看這段代碼,看看發(fā)生了什么:
export default function Square() {
return <button className="square">X</button>;
}
第一行定義了一個名為 Square 的函數(shù)。JavaScript 的 export 關(guān)鍵字使此函數(shù)可以在此文件之外訪問。default 關(guān)鍵字表明它是文件中的主要函數(shù)。
第二行返回一個按鈕。JavaScript 的 return 關(guān)鍵字意味著后面的內(nèi)容都作為值返回給函數(shù)的調(diào)用者。<button> 是一個 JSX 元素。JSX 元素是 JavaScript 代碼和 HTML 標(biāo)簽的組合,用于描述要顯示的內(nèi)容。className="square" 是一個 button 屬性,它決定 CSS 如何設(shè)置按鈕的樣式。X 是按鈕內(nèi)顯示的文本,</button> 閉合 JSX 元素以表示不應(yīng)將任何后續(xù)內(nèi)容放置在按鈕內(nèi)。
styles.css
該文件定義了 React 應(yīng)用的樣式。
前兩個 CSS 選擇器(* 和 body)定義了應(yīng)用大部分的樣式,而 .square 選擇器定義了 className 屬性設(shè)置為 square 的組件的樣式。這與 App.js 文件中的 Square 組件中的按鈕是相匹配的。
index.js
它是 App.js 文件中創(chuàng)建的組件與 Web 瀏覽器之間的橋梁。
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
第 1-5 行將所有必要的部分組合在一起:
- React
- React 與 Web 瀏覽器對話的庫(React DOM)
- 組件的樣式
- App.js 里面創(chuàng)建的組件
其他文件將它們組合在一起,并將最終成果注入 public 文件夾里面的 index.html 中
構(gòu)建棋盤
React 組件必須返回單個 JSX 元素,不能像兩個按鈕那樣返回多個相鄰的 JSX 元素。要解決此問題,可以使用 Fragment(<> 與 </>)包裹多個相鄰的 JSX 元素,如下所示:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
現(xiàn)在你應(yīng)該可以看見:

非常棒!現(xiàn)在你只需要通過復(fù)制粘貼來添加九個方塊,然后……

但事與愿違的是這些方塊并沒有排列成網(wǎng)格,而是都在一條線上。要解決此問題,需要使用 div 將方塊分到每一行中并添加一些 CSS 樣式。當(dāng)你這樣做的時候,需要給每個方塊一個數(shù)字,以確保你知道每個方塊的位置。
App.js 文件中,Square 組件看起來像這樣:
export default function Square() {
return (
<>
<div className="board-row">
<button className="square">1</button>
<button className="square">2</button>
<button className="square">3</button>
</div>
<div className="board-row">
<button className="square">4</button>
<button className="square">5</button>
<button className="square">6</button>
</div>
<div className="board-row">
<button className="square">7</button>
<button className="square">8</button>
<button className="square">9</button>
</div>
</>
);
}
借助 styles.css 中定義的 board-row 樣式,我們將組件分到每一行的 div 中。最終完成了井字棋棋盤:

但是現(xiàn)在有個問題,名為 Square 的組件實(shí)際上不再是方塊了。讓我們通過將名稱更改為 Board 來解決這個問題:
export default function Board() {
//...
}
通過props傳遞數(shù)據(jù)
接下來,當(dāng)用戶單擊方塊時,我們要將方塊的值從空更改為“X”。根據(jù)目前構(gòu)建的棋盤,你需要復(fù)制并粘貼九次更新方塊的代碼(每個方塊都需要一次)!但是,React 的組件架構(gòu)可以創(chuàng)建可重用的組件,以避免混亂、重復(fù)的代碼。
首先,要將定義第一個方塊(<button className="square">1</button>)的這行代碼從 Board 組件復(fù)制到新的 Square 組件中:
function Square() {
return <button className="square">1</button>;
}
然后,更新 Board 組件并使用 JSX 語法渲染 Square 組件:
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
需要注意的是,這并不像 div,這些你自己的組件如 Board 和 Square,必須以大寫字母開頭。

你失去了你以前有正確編號的方塊?,F(xiàn)在每個方塊都寫著“1”。要解決此問題,需要使用 props 將每個方塊應(yīng)有的值從父組件(Board)傳遞到其子組件(Square)。
更新 Square 組件,讀取從 Board 傳遞的 value props:
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value }) 表示可以向 Square 組件傳遞一個名為 value 的 props。
我們需要從組件中渲染名為 value 的 JavaScript 變量,而不是“value”這個詞。要從 JSX“轉(zhuǎn)義到 JavaScript”,你需要使用大括號。在 JSX 中的 value 周圍添加大括號,如下所示:
function Square({ value }) {
return <button className="square">{value}</button>;
}
export default function Board() {
return (
<>
<div className="board-row">
<Square value="1" />
<Square value="2" />
<Square value="3" />
</div>
<div className="board-row">
<Square value="4" />
<Square value="5" />
<Square value="6" />
</div>
<div className="board-row">
<Square value="7" />
<Square value="8" />
<Square value="9" />
</div>
</>
);
}

創(chuàng)建一個具有交互性的組件
當(dāng)你單擊它的時候,Square 組件需要顯示“X”。在 Square 內(nèi)部聲明一個名為 handleClick 的函數(shù)。然后,將 onClick 添加到由 Square 返回的 JSX 元素的 button 的 props 中:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
如果現(xiàn)在單擊一個方塊,你應(yīng)該會看到一條日志,上面寫著 "clicked!"
下一步,我們希望 Square 組件能夠“記住”它被單擊過,并用“X”填充它。為了“記住”一些東西,組件使用 state。
React 提供了一個名為 useState 的特殊函數(shù),可以從組件中調(diào)用它來讓它“記住”一些東西。讓我們將 Square 的當(dāng)前值存儲在 state 中,并在單擊 Square 時更改它。
在文件的頂部導(dǎo)入 useState。從 Square 組件中移除 value props。在調(diào)用 useState 的 Square 的開頭添加一個新行。讓它返回一個名為 value 的 state 變量:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value 存儲值,而 setValue 是可用于更改值的函數(shù)。傳遞給 useState 的 null 用作這個 state 變量的初始值,因此此處 value 的值開始時等于 null。
由于 Square 組件不再接受 props,我們從 Board 組件創(chuàng)建的所有九個 Square 組件中刪除 value props:
export default function Board() {
return (
<>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
<div className="board-row">
<Square />
<Square />
<Square />
</div>
</>
);
}
現(xiàn)在將更改 Square 以在單擊時顯示“X”。不再使用 console.log("clicked!"); 而使用 setValue('X'); 的事件處理程序?,F(xiàn)在你的 Square 組件看起來像這樣:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
通過從 onClick 處理程序調(diào)用此 set 函數(shù),你告訴 React 在單擊其 <button> 時要重新渲染該 Square。更新后,方塊的值將為“X”,因此會在棋盤上看到“X”。
每個 Square 都有自己的 state:存儲在每個 Square 中的 value 完全獨(dú)立于其他的 Square。當(dāng)你在組件中調(diào)用 set 函數(shù)時,React 也會自動更新內(nèi)部的子組件。
React開發(fā)者工具
React 開發(fā)者工具可以檢查 React 組件的 props 和 state
要檢查屏幕上的特定組件,請使用 React 開發(fā)者工具左上角的按鈕
狀態(tài)提升
目前,每個 Square 組件都維護(hù)著游戲 state 的一部分。要檢查井字棋游戲中的贏家,Board 需要以某種方式知道 9 個 Square 組件中每個組件的 state。
你會如何處理?起初,你可能會猜測 Board 需要向每個 Square“詢問”Square 的 state。盡管這種方法在 React 中在技術(shù)上是可行的,但我們不鼓勵這樣做,因?yàn)榇a變得難以理解、容易出現(xiàn)錯誤并且難以重構(gòu)。相反,最好的方法是將游戲的 state 存儲在 Board 父組件中,而不是每個 Square 中。Board 組件可以通過傳遞一個 props 來告訴每個 Square 顯示什么,就像你將數(shù)字傳遞給每個 Square 時所做的那樣。
要從多個子組件收集數(shù)據(jù),或讓兩個子組件相互通信,請改為在其父組件中聲明共享 state。父組件可以通過 props 將該 state 傳回給子組件。這使子組件彼此同步并與其父組件保持同步。
重構(gòu) React 組件時,將狀態(tài)提升到父組件中很常見。
編輯 Board 組件,使其聲明一個名為 squares 的 state 變量,該變量默認(rèn)為對應(yīng)于 9 個方塊的 9 個空值數(shù)組:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null) 創(chuàng)建了一個包含九個元素的數(shù)組,并將它們中的每一個都設(shè)置為 null。包裹它的 useState() 聲明了一個初始設(shè)置為該數(shù)組的 squares state 變量。數(shù)組中的每個元素對應(yīng)于一個 square 的值
現(xiàn)在你的 Board 組件需要將 value props 向下傳遞給它渲染的每個 Square:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} />
<Square value={squares[1]} />
<Square value={squares[2]} />
</div>
<div className="board-row">
<Square value={squares[3]} />
<Square value={squares[4]} />
<Square value={squares[5]} />
</div>
<div className="board-row">
<Square value={squares[6]} />
<Square value={squares[7]} />
<Square value={squares[8]} />
</div>
</>
);
}
接下來,你將編輯 Square 組件,以從 Board 組件接收 value props。這將需要刪除 Square 組件自己的 value state 和按鈕的 onClick props:
function Square({value}) {
return <button className="square">{value}</button>;
}

現(xiàn)在,每個 Square 都會收到一個 value props,對于空方塊,該 props 將是 'X'、'O' 或 null。
接下來,你需要更改單擊 Square 時發(fā)生的情況。Board 組件現(xiàn)在維護(hù)已經(jīng)填充過的方塊。你需要為 Square 創(chuàng)建一種更新 Board state 的方法。由于 state 對于定義它的組件是私有的,因此你不能直接從 Square 更新 Board 的 state。
你將從 Board 組件向下傳遞一個函數(shù)到 Square 組件,然后讓 Square 在單擊方塊時調(diào)用該函數(shù)。我們將從單擊 Square 組件時將調(diào)用的函數(shù)開始。調(diào)用該函數(shù) onSquareClick:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
接下來,將 onSquareClick 函數(shù)添加到 Square 組件的 props 中:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
現(xiàn)在,你將把 onSquareClick props 連接到 Board 組件中的一個函數(shù),命名為 handleClick。要將 onSquareClick 連接到 handleClick,需要將一個函數(shù)傳遞給第一個 Square 組件的 onSquareClick props:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
最后,你將在 Board 組件內(nèi)定義 handleClick 函數(shù)來更新并保存棋盤 state 的 squares 數(shù)組:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick() {
const nextSquares = squares.slice();
nextSquares[0] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
handleClick 函數(shù)使用 JavaScript 數(shù)組的 slice() 方法創(chuàng)建 squares 數(shù)組的副本(nextSquares)。然后,handleClick 更新 nextSquares 數(shù)組,將 X 添加到第一個([0] 索引)方塊。
調(diào)用 setSquares 函數(shù)讓 React 知道組件的 state 已經(jīng)改變。這將觸發(fā)使用 squares state 的組件(Board)及其子組件(構(gòu)成棋盤的 Square 組件)的重新渲染。
將參數(shù) i 添加到 handleClick 函數(shù),該函數(shù)采用要更新的 square 索引:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
// ...
)
}
接下來,你需要將 i 傳遞給 handleClick。你可以嘗試像這樣在 JSX 中直接將 square 的 onSquareClick props 設(shè)置為 handleClick(0),但這是行不通的:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
為什么會是這樣呢?handleClick(0) 調(diào)用將成為渲染 Board 組件的一部分。因?yàn)?handleClick(0) 通過調(diào)用 setSquares 改變了棋盤組件的 state,所以你的整個棋盤組件將再次重新渲染。但這再次運(yùn)行了 handleClick(0),導(dǎo)致無限循環(huán):

為什么這個問題沒有早點(diǎn)發(fā)生?
當(dāng)你傳遞 onSquareClick={handleClick} 時,你將 handleClick 函數(shù)作為 props 向下傳遞。你不是在調(diào)用它!但是現(xiàn)在你立即調(diào)用了該函數(shù)——注意 handleClick(0) 中的括號——這就是它運(yùn)行得太早的原因。你不想在用戶點(diǎn)擊之前調(diào)用 handleClick!
你可以通過創(chuàng)建調(diào)用 handleClick(0) 的函數(shù)(如 handleFirstSquareClick)、調(diào)用 handleClick(1) 的函數(shù)(如 handleSecondSquareClick)等來修復(fù)。你可以將這些函數(shù)作為 onSquareClick={handleFirstSquareClick} 之類的 props 傳遞(而不是調(diào)用)。這將解決無限循環(huán)的問題。
但是,定義九個不同的函數(shù)并為每個函數(shù)命名過于冗余。讓我們這樣做:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
注意新的 () => 語法。這里,() => handleClick(0) 是一個箭頭函數(shù),它是定義函數(shù)的一種較短的方式。單擊方塊時,=>“箭頭”之后的代碼將運(yùn)行,調(diào)用 handleClick(0)。
現(xiàn)在你需要更新其他八個方塊以從你傳遞的箭頭函數(shù)中調(diào)用 handleClick。確保 handleClick 的每次調(diào)用的參數(shù)對應(yīng)于正確的 square 索引:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
};
現(xiàn)在,我們在 Board 組件中處理 state, Board 父組件將 props 傳遞給 Square 子組件,以便它們可以正確顯示。單擊 Square 時, Square 子組件現(xiàn)在要求 Board 父組件更新棋盤的 state。當(dāng) Board 的 state 改變時,Board 組件和每個子 Square 都會自動重新渲染。保存 Board 組件中所有方塊的 state 將使得它可以確定未來的贏家。
讓我們回顧一下當(dāng)用戶單擊你的棋盤左上角的方塊以向其添加 X 時會發(fā)生什么:
- 單擊左上角的方塊運(yùn)行 button 從 Square 接收到的 onClick props 的函數(shù)。Square 組件從 Board 通過 onSquareClick props 接收到該函數(shù)。Board 組件直接在 JSX 中定義了該函數(shù)。它使用參數(shù) 0 調(diào)用 handleClick。
- handleClick 使用參數(shù)(0)將 squares 數(shù)組的第一個元素從 null 更新為 X。
- Board 組件的 squares state 已更新,因此 Board 及其所有子組件都將重新渲染。這會導(dǎo)致索引為 0 的 Square 組件的 value props 從 null 更改為 X。
注意:
DOM <button> 元素的 onClick props 對 React 有特殊意義,因?yàn)樗且粋€內(nèi)置組件。對于像 Square 這樣的自定義組件,命名由你決定。你可以給 Square 的 onSquareClick props 或 Board 的 handleClick 函數(shù)起任何名字,代碼還是可以運(yùn)行的。在 React 中,通常使用 onSomething 命名代表事件的 props,使用 handleSomething 命名處理這些事件的函數(shù)。
為什么不變性很重要
請注意在 handleClick 中,你調(diào)用了 .slice() 來創(chuàng)建 squares 數(shù)組的副本而不是修改現(xiàn)有數(shù)組。為了解釋原因,我們需要討論不變性以及為什么學(xué)習(xí)不變性很重要。
通常有兩種更改數(shù)據(jù)的方法。第一種方法是通過直接更改數(shù)據(jù)的值來改變數(shù)據(jù)。第二種方法是使用具有所需變化的新副本替換數(shù)據(jù)。如果你改變 squares 數(shù)組,它會是這樣的:
const squares = [null, null, null, null, null, null, null, null, null];
squares[0] = 'X';
// Now `squares` is ["X", null, null, null, null, null, null, null, null];
如果你在不改變 squares 數(shù)組的情況下更改數(shù)據(jù),它會是這樣的:
const squares = [null, null, null, null, null, null, null, null, null];
const nextSquares = ['X', null, null, null, null, null, null, null, null];
// Now `squares` is unchanged, but `nextSquares` first element is 'X' rather than `null`
結(jié)果是一樣的,但通過不直接改變(改變底層數(shù)據(jù)),你可以獲得幾個好處。
不變性使復(fù)雜的功能更容易實(shí)現(xiàn)。在本教程的后面,你將實(shí)現(xiàn)一個“時間旅行”功能,讓你回顧游戲的歷史并“跳回”到過去的動作。此功能并非特定于游戲——撤消和重做某些操作的能力是應(yīng)用程序的常見要求。避免數(shù)據(jù)直接突變可以讓你保持以前版本的數(shù)據(jù)完好無損,并在以后重用它們。
不變性還有另一個好處。默認(rèn)情況下,當(dāng)父組件的 state 發(fā)生變化時,所有子組件都會自動重新渲染。這甚至包括未受變化影響的子組件。盡管重新渲染本身不會引起用戶注意(你不應(yīng)該主動嘗試避免它?。鲇谛阅茉?,你可能希望跳過重新渲染顯然不受其影響的樹的一部分。不變性使得組件比較其數(shù)據(jù)是否已更改的成本非常低。你可以在 memo API 參考 中了解更多關(guān)于 React 如何選擇何時重新渲染組件的信息。
交替落子
默認(rèn)情況下,你會將第一步設(shè)置為“X”。讓我們通過向 Board 組件添加另一個 state 來跟蹤這一點(diǎn):
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
每次玩家落子時,xIsNext(一個布爾值)將被翻轉(zhuǎn)以確定下一個玩家,游戲 state 將被保存。你將更新 Board 的 handleClick 函數(shù)以翻轉(zhuǎn) xIsNext 的值:
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
return (
//...
);
}
現(xiàn)在,當(dāng)你點(diǎn)擊不同的方塊時,它們會在 X 和 O 之間交替,這是它們應(yīng)該做的!
但是有一個問題。嘗試多次點(diǎn)擊同一個方塊:
X 被 O 覆蓋!雖然這會給游戲帶來非常有趣的變化,但我們現(xiàn)在將堅(jiān)持原來的規(guī)則。
當(dāng)你用 X 或 O 標(biāo)記方塊時,你沒有檢查該方塊是否已經(jīng)具有 X 或 O 值。你可以通過提早返回來解決此問題。我們將檢查方塊是否已經(jīng)有 X 或 O。如果方塊已經(jīng)填滿,你將盡早在 handleClick 函數(shù)中 return——在它嘗試更新棋盤 state 之前。
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
現(xiàn)在你只能將 X 或 O 添加到空方塊中!此時你的代碼應(yīng)該如下所示:
import { useState } from 'react';
function Square({value, onSquareClick}) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
宣布獲勝者
現(xiàn)在你可以輪流對戰(zhàn)了,接下來我們應(yīng)該顯示游戲何時獲勝。為此,你將添加一個名為 calculateWinner 的輔助函數(shù),它接受 9 個方塊的數(shù)組,檢查獲勝者并根據(jù)需要返回 'X'、'O' 或 null。不要太擔(dān)心 calculateWinner 函數(shù);它不是 React 才會有的:
export default function Board() {
//...
}
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;
}
你將在 Board 組件的 handleClick 函數(shù)中調(diào)用 calculateWinner(squares) 來檢查玩家是否獲勝。你可以在檢查用戶是否單擊了已經(jīng)具有 X 或 O 的方塊的同時執(zhí)行此檢查。在這兩種情況下,我們都希望盡早返回:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
為了讓玩家知道游戲何時結(jié)束,你可以顯示“獲勝者:X”或“獲勝者:O”等文字。為此,你需要將 status 部分添加到 Board 組件。如果游戲結(jié)束,將顯示獲勝者,如果游戲正在進(jìn)行,你將顯示下一輪將會是哪個玩家:
export default function Board() {
// ...
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
// ...
)
}
恭喜!你現(xiàn)在有一個可以運(yùn)行的井字棋游戲。你也學(xué)習(xí)了 React 的基礎(chǔ)知識。所以你是這里真正的贏家。代碼應(yīng)該如下所示:
import { useState } from 'react';
function Square({value, onSquareClick}) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
export default function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
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í),讓我們能夠“回到”到游戲中之前的動作。
存儲落子歷史
如果你改變了 squares 數(shù)組,實(shí)現(xiàn)時間旅行將非常困難。
但是,你在每次落子后都使用 slice() 創(chuàng)建 squares 數(shù)組的新副本,并將其視為不可變的。這將允許你存儲 squares 數(shù)組的每個過去的版本,并在已經(jīng)發(fā)生的輪次之間“來回”。
把過去的 squares 數(shù)組存儲在另一個名為 history 的數(shù)組中,把它存儲為一個新的 state 變量。history 數(shù)組表示所有棋盤的 state,從第一步到最后一步,其形狀如下:
[
// Before first move
[null, null, null, null, null, null, null, null, null],
// After first move
[null, null, null, null, 'X', null, null, null, null],
// After second move
[null, null, null, null, 'X', null, null, null, 'O'],
// ...
]
再一次“狀態(tài)提升”
你現(xiàn)在將編寫一個名為 Game 的新頂級組件來顯示過去的著法列表。這就是放置包含整個游戲歷史的 history state 的地方。
將 history state 放入 Game 組件將使你可以從其 Board 子組件中刪除 squares state。就像你將 state 從 Square 組件“提升”到 Board 組件一樣,你現(xiàn)在將把它從 Board 提升到頂層 Game 組件。這使 Game 組件可以完全控制 Board 的數(shù)據(jù),并使它讓 Board 渲染來自 history 的之前的回合。
首先,添加一個帶有 export default 的 Game 組件。讓它渲染 Board 組件和一些標(biāo)簽:
function Board() {
// ...
}
export default function Game() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
請注意,你要刪除 function Board() { 聲明之前的 export default 關(guān)鍵字,并將它們添加到 function Game() { 聲明之前。這會告訴你的 index.js 文件使用 Game 組件而不是你的 Board 組件作為頂層組件。Game 組件返回的額外 div 正在為你稍后添加到棋盤的游戲信息騰出空間。
向 Game 組件添加一些 state 以跟蹤下一個玩家和落子歷史:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
請注意,[Array(9).fill(null)] 是一個包含單個元素的數(shù)組,它本身是一個包含 9 個 null 的數(shù)組。
要渲染當(dāng)前落子的方塊,你需要從 history 中讀取最后一個 squares 數(shù)組。你不需要 useState——你已經(jīng)有足夠的信息可以在渲染過程中計(jì)算它:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
接下來,在 Game 組件中創(chuàng)建一個 handlePlay 函數(shù),Board 組件將調(diào)用該函數(shù)來更新游戲。將 xIsNext、currentSquares 和 handlePlay 作為 props 傳遞給 Board 組件:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
// TODO
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
//...
)
}
讓 Board 組件完全由它接收到的 props 控制。更改 Board 組件以采用三個 props:xIsNext、squares和一個新的 onPlay 函數(shù),當(dāng)玩家落子時,Board 可以使用更新的 square 數(shù)組調(diào)用該函數(shù)。接下來,刪除調(diào)用 useState 的 Board 函數(shù)的前兩行:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
現(xiàn)在,將 Board 組件里面的 handleClick 中的 setSquares 和 setXIsNext 調(diào)用替換為對新 onPlay 函數(shù)的一次調(diào)用,這樣 Game 組件就可以在用戶單擊方塊時更新 Board:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
onPlay(nextSquares);
}
//...
}
Board 組件完全由 Game 組件傳遞給它的 props 控制。你需要在 Game 組件中實(shí)現(xiàn) handlePlay 函數(shù)才能使游戲重新運(yùn)行。
handlePlay 被調(diào)用應(yīng)該做什么?請記住,Board 以前使用更新后的數(shù)組調(diào)用 setSquares;現(xiàn)在它將更新后的 squares 數(shù)組傳遞給 onPlay。
handlePlay 函數(shù)需要更新 Game 的 state 以觸發(fā)重新渲染,但是你沒有可以再調(diào)用的 setSquares 函數(shù)——你現(xiàn)在正在使用 history state 變量來存儲這些信息。你需要追加更新后的 squares 數(shù)組來更新 history 作為新的歷史入口。你還需要切換 xIsNext,就像 Board 過去所做的那樣:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
在這里,[...history, nextSquares] 創(chuàng)建了一個新數(shù)組,其中包含 history 中的所有元素,后跟 nextSquares。(你可以將 ...history 展開語法理解為“枚舉 history 中的所有元素”。)
例如,如果 history 是 [[null,null,null], ["X",null,null]],nextSquares 是 ["X",null,"O"],那么新的 [...history, nextSquares] 數(shù)組就是 [[null,null,null], ["X",null,null], ["X",null,"O"]]。
此時,你已將 state 移至 Game 組件中,并且 UI 應(yīng)該完全正常工作,就像重構(gòu)之前一樣。這是此時代碼的樣子:
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
if (calculateWinner(squares) || squares[i]) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = 'X';
} else {
nextSquares[i] = 'O';
}
onPlay(nextSquares);
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{/*TODO*/}</ol>
</div>
</div>
);
}
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;
}
顯示過去的落子
由于你正在記錄井字棋游戲的歷史,因此你現(xiàn)在可以向玩家顯示過去的動作列表。
像 <button> 這樣的 React 元素是常規(guī)的 JavaScript 對象;你可以在你的應(yīng)用程序中傳遞它們。要在 React 中渲染多個項(xiàng)目,你可以使用 React 元素?cái)?shù)組。
你已經(jīng)有一組 state 為 history 的數(shù)組,所以現(xiàn)在你需要將其轉(zhuǎn)換為一組 React 元素。在 JavaScript 中,要將一個數(shù)組轉(zhuǎn)換為另一個數(shù)組,可以使用 數(shù)組的 map 方法。
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
你將使用 map 將你的 history 動作轉(zhuǎn)換為代表屏幕上按鈕的 React 元素,并顯示一個按鈕列表以“跳轉(zhuǎn)”到過去的動作。讓我們在 Game 組件中用 map 代替 history:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
function jumpTo(nextMove) {
// TODO
}
const moves = history.map((squares, move) => {
let description;
if (move > 0) {
description = 'Go to move #' + move;
} else {
description = 'Go to game start';
}
return (
<li>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{moves}</ol>
</div>
</div>
);
}
你可以在下面看到你的代碼應(yīng)該是什么樣子。請注意,你應(yīng)該會在開發(fā)者工具控制臺中看到一條錯誤消息:

當(dāng)你在傳遞給 map 的函數(shù)中遍歷 history 數(shù)組時,squares 參數(shù)遍歷 history 的每個元素,move 參數(shù)遍歷每個數(shù)組索引:0 、1、2……(在大多數(shù)情況下,你需要數(shù)組元素,但要渲染落子列表,你只需要索引。)
對于井字棋游戲歷史中的每一步,你創(chuàng)建一個列表項(xiàng) <li>,其中包含一個按鈕 <button>。該按鈕有一個 onClick 處理程序,它調(diào)用一個名為 jumpTo 的函數(shù)(你尚未實(shí)現(xiàn))。
現(xiàn)在,你應(yīng)該會看到游戲中發(fā)生的動作列表和開發(fā)人員工具控制臺中的錯誤。讓我們討論一下“關(guān)鍵”錯誤的含義。
選擇 key
當(dāng)你渲染一個列表時,React 會存儲一些關(guān)于每個渲染列表項(xiàng)的信息。當(dāng)你更新一個列表時,React 需要確定發(fā)生了什么變化。你可以添加、刪除、重新排列或更新列表的項(xiàng)目。
想象一下從
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
到
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
除了更新的計(jì)數(shù)之外,閱讀本文的人可能會說你交換了 Alexa 和 Ben 的順序,并在 Alexa 和 Ben 之間插入了 Claudia。然而,React 是一個計(jì)算機(jī)程序,不知道你的意圖,因此你需要為每個列表項(xiàng)指定一個 key 屬性,以將每個列表項(xiàng)與其兄弟項(xiàng)區(qū)分開來。如果你的數(shù)據(jù)來自數(shù)據(jù)庫,Alexa、Ben 和 Claudia 的數(shù)據(jù)庫 ID 可以用作 key:
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
重新渲染列表時,React 獲取每個列表項(xiàng)的 key 并搜索前一個列表的項(xiàng)以查找匹配的 key。如果當(dāng)前列表有一個之前不存在的 key,React 會創(chuàng)建一個組件。如果當(dāng)前列表缺少前一個列表中存在的 key,React 會銷毀前一個組件。如果兩個 key 匹配,則落子相應(yīng)的組件。
key 告訴 React 每個組件的身份,這使得 React 可以在重新渲染時保持 state。如果組件的 key 發(fā)生變化,組件將被銷毀,新 state 將重新創(chuàng)建。
key 是 React 中一個特殊的保留屬性。創(chuàng)建元素時,React 提取 key 屬性并將 key 直接存儲在返回的元素上。盡管 key 看起來像是作為 props 傳遞的,但 React 會自動使用 key 來決定要更新哪些組件。組件無法詢問其父組件指定的 key。
強(qiáng)烈建議你在構(gòu)建動態(tài)列表時分配適當(dāng)?shù)?key。如果你沒有合適的 key,你可能需要考慮重組你的數(shù)據(jù),以便你這樣做。
如果沒有指定 key,React 會報錯,默認(rèn)使用數(shù)組索引作為 key。在嘗試重新排序列表項(xiàng)或插入/刪除列表項(xiàng)時,使用數(shù)組索引作為 key 是有問題的。顯式傳遞 key={i} 可以消除錯誤,但與數(shù)組索引有相同的問題,在大多數(shù)情況下不推薦使用。
key 不需要是全局唯一的;它們只需要在組件及其同級組件之間是唯一的。
實(shí)現(xiàn)時間旅行
在井字棋游戲的歷史中,過去的每一步都有一個唯一的 ID 與之相關(guān)聯(lián):它是動作的序號。落子永遠(yuǎn)不會被重新排序、刪除或從中間插入,因此使用落子的索引作為 key 是安全的。
在 Game 函數(shù)中,你可以將 key 添加為 <li key={move}>,如果你重新加載渲染的游戲,React 的“key”錯誤應(yīng)該會消失:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
在你可以實(shí)現(xiàn) jumpTo 之前,你需要 Game 組件來跟蹤用戶當(dāng)前正在查看的步驟。為此,定義一個名為 currentMove 的新 state 變量,默認(rèn)為 0:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[history.length - 1];
//...
}
接下來,更新 Game 中的 jumpTo 函數(shù)來更新 currentMove。如果你將 currentMove 更改為偶數(shù),你還將設(shè)置 xIsNext 為 true。
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
你現(xiàn)在將對 Game 的 handlePlay 函數(shù)進(jìn)行兩處更改,該函數(shù)在你單擊方塊時調(diào)用。
- 如果你“回到過去”然后從那一點(diǎn)開始采取新的行動,你只想保持那一點(diǎn)的歷史。不是在 history 中的所有項(xiàng)目(... 擴(kuò)展語法)之后添加 nextSquares,而是在 history.slice(0, currentMove + 1) 中的所有項(xiàng)目之后添加它,這樣你就只保留舊歷史的那部分。
- 每次落子時,你都需要更新 currentMove 以指向最新的歷史條目。
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
最后,你將修改 Game 組件以渲染當(dāng)前選定的著法,而不是始終渲染最后的著法:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[currentMove];
// ...
}
如果你點(diǎn)擊游戲歷史中的任何一步,井字棋棋盤應(yīng)立即更新以顯示該步驟發(fā)生后棋盤的樣子。

最后清理
如果仔細(xì)查看代碼,你可能會注意到當(dāng) currentMove 為偶數(shù)時為 xIsNext === true,而當(dāng) currentMove 為奇數(shù)時為 xIsNext === false。換句話說,如果你知道 currentMove 的值,那么你總能算出 xIsNext 應(yīng)該是什么。
你沒有理由將這兩者都存儲在 state 中。事實(shí)上,總是盡量避免冗余的 state。簡化你在 state 中存儲的內(nèi)容可以減少錯誤并使你的代碼更易于理解。更改 Game 使其不將 xIsNext 存儲為單獨(dú)的 state 變量,而是根據(jù) currentMove 計(jì)算出來:
export default function Game() {
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
}
function jumpTo(nextMove) {
setCurrentMove(nextMove);
}
// ...
}
你不再需要 xIsNext state 聲明或?qū)?setXIsNext 的調(diào)用?,F(xiàn)在,xIsNext 不可能與 currentMove 不同步,即使你的代碼寫錯了。
收尾
祝賀!你已經(jīng)創(chuàng)建了一個井字棋游戲,你實(shí)現(xiàn)了:
- 你現(xiàn)在可以玩的井字棋游戲
- 玩家在贏的時候有提示
- 隨著游戲的進(jìn)行存儲游戲的歷史
- 允許玩家回顧游戲的歷史并查看棋盤的以前的版本
干得好!我們希望你現(xiàn)在覺得你對 React 的工作原理有了很好的了解。