在本教程中,你将构建一个小型井字游戏。本教程不假设任何现有的 React 知识。你将在本教程中学习的技术是构建任何 React 应用的基础,充分理解它将使你对 React 有更深入的了解。
¥You will build a small tic-tac-toe game during this tutorial. This tutorial does not assume any existing React knowledge. The techniques you’ll learn in the tutorial are fundamental to building any React app, and fully understanding it will give you a deep understanding of React.
本教程分为几个部分:
¥The tutorial is divided into several sections:
-
教程的设置 将为你提供学习本教程的起点。
¥Setup for the tutorial will give you a starting point to follow the tutorial.
-
概述 将教你 React 的基础知识:组件、属性和状态。
¥Overview will teach you the fundamentals of React: components, props, and state.
-
完成游戏 将教你 React 开发中最常用的技术。
¥Completing the game will teach you the most common techniques in React development.
-
添加时光旅行 将让你更深入地了解 React 的独特优势。
¥Adding time travel will give you a deeper insight into the unique strengths of React.
你在构建什么?
¥What are you building?
在本教程中,你将使用 React 构建交互式井字游戏。
¥In this tutorial, you’ll build an interactive tic-tac-toe game with React.
你可以在此处查看完成后的样子:
¥You can see what it will look like when you’re finished here:
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; }
如果代码对你来说还没有意义,或者如果你不熟悉代码的语法,请不要担心!本教程的目标是帮助你理解 React 及其语法。
¥If the code doesn’t make sense to you yet, or if you are unfamiliar with the code’s syntax, don’t worry! The goal of this tutorial is to help you understand React and its syntax.
我们建议你在继续本教程之前查看上面的井字游戏。你会注意到的一项功能是游戏面板右侧有一个编号列表。此列表为你提供了游戏中发生的所有动作的历史记录,并随着游戏的进行而更新。
¥We recommend that you check out the tic-tac-toe game above before continuing with the tutorial. One of the features that you’ll notice is that there is a numbered list to the right of the game’s board. This list gives you a history of all of the moves that have occurred in the game, and it is updated as the game progresses.
玩完已完成的井字游戏后,请继续滚动。在本教程中,你将从一个更简单的模板开始。我们的下一步是为你进行设置,以便你可以开始构建游戏。
¥Once you’ve played around with the finished tic-tac-toe game, keep scrolling. You’ll start with a simpler template in this tutorial. Our next step is to set you up so that you can start building the game.
教程的设置
¥Setup for the tutorial
在下面的实时代码编辑器中,单击右上角的 Fork,以使用网站 CodeSandbox 在新选项卡中打开编辑器。CodeSandbox 让你可以在浏览器中编写代码并预览你的用户将如何看待你创建的应用。新选项卡应显示一个空方块和本教程的起始代码。
¥In the live code editor below, click Fork in the top-right corner to open the editor in a new tab using the website CodeSandbox. CodeSandbox lets you write code in your browser and preview how your users will see the app you’ve created. The new tab should display an empty square and the starter code for this tutorial.
export default function Square() { return <button className="square">X</button>; }
概述
¥Overview
现在你已经设置好了,让我们来了解一下 React!
¥Now that you’re set up, let’s get an overview of React!
检查起始代码
¥Inspecting the starter code
在 CodeSandbox 中,你将看到三个主要部分:
¥In CodeSandbox you’ll see three main sections:
-
文件部分包含文件列表,例如
App.js
、index.js
、styles.css
和一个名为public
的文件夹¥The Files section with a list of files like
App.js
,index.js
,styles.css
and a folder calledpublic
-
代码编辑器,你将在其中看到所选文件的源代码
¥The code editor where you’ll see the source code of your selected file
-
浏览器部分,你将在其中看到你编写的代码将如何显示
¥The browser section where you’ll see how the code you’ve written will be displayed
应在文件部分选择 App.js
文件。代码编辑器中该文件的内容应该是:
¥The App.js
file should be selected in the Files section. The contents of that file in the code editor should be:
export default function Square() {
return <button className="square">X</button>;
}
浏览器部分应该显示一个带有 X 的方块,如下所示:
¥The browser section should be displaying a square with a X in it like this:
现在让我们看一下起始代码中的文件。
¥Now let’s have a look at the files in the starter code.
App.js
App.js
中的代码创建了一个组件。在 React 中,组件是代表用户界面一部分的一段可重用代码。组件用于渲染、管理和更新应用中的 UI 元素。让我们逐行查看组件,看看发生了什么:
¥The code in App.js
creates a component. In React, a component is a piece of reusable code that represents a part of a user interface. Components are used to render, manage, and update the UI elements in your application. Let’s look at the component line by line to see what’s going on:
export default function Square() {
return <button className="square">X</button>;
}
第一行定义了一个名为 Square
的函数。export
JavaScript 关键字使此函数可以在此文件之外访问。default
关键字告诉其他使用你的代码的文件它是你文件中的主要函数。
¥The first line defines a function called Square
. The export
JavaScript keyword makes this function accessible outside of this file. The default
keyword tells other files using your code that it’s the main function in your file.
export default function Square() {
return <button className="square">X</button>;
}
第二行返回一个按钮。return
JavaScript 关键字表示后面的任何内容都作为值返回给函数的调用者。<button>
是一个 JSX 元素。JSX 元素是 JavaScript 代码和 HTML 标记的组合,用于描述你想要显示的内容。className="square"
是一个按钮属性或属性,它告诉 CSS 如何设置按钮的样式。X
是按钮内显示的文本,</button>
闭合 JSX 元素以指示不应将任何后续内容放置在按钮内。
¥The second line returns a button. The return
JavaScript keyword means whatever comes after is returned as a value to the caller of the function. <button>
is a JSX element. A JSX element is a combination of JavaScript code and HTML tags that describes what you’d like to display. className="square"
is a button property or prop that tells CSS how to style the button. X
is the text displayed inside of the button and </button>
closes the JSX element to indicate that any following content shouldn’t be placed inside the button.
styles.css
单击 CodeSandbox 的文件部分中标记为 styles.css
的文件。该文件定义了 React 应用的样式。前两个 CSS 选择器(*
和 body
)定义应用大部分的样式,而 .square
选择器定义 className
属性设置为 square
的任何组件的样式。在你的代码中,这将匹配 App.js
文件中 Square 组件的按钮。
¥Click on the file labeled styles.css
in the Files section of CodeSandbox. This file defines the styles for your React app. The first two CSS selectors (*
and body
) define the style of large parts of your app while the .square
selector defines the style of any component where the className
property is set to square
. In your code, that would match the button from your Square component in the App.js
file.
index.js
单击 CodeSandbox 的文件部分中标记为 index.js
的文件。你不会在本教程中编辑此文件,但它是你在 App.js
文件中创建的组件与 Web 浏览器之间的桥梁。
¥Click on the file labeled index.js
in the Files section of CodeSandbox. You won’t be editing this file during the tutorial but it is the bridge between the component you created in the App.js
file and the web browser.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
第 1-5 行将所有必要的部分组合在一起:
¥Lines 1-5 bring all the necessary pieces together:
-
React
-
React 与网络浏览器对话的库 (React DOM)
¥React’s library to talk to web browsers (React DOM)
-
组件的样式
¥the styles for your components
-
你在
App.js
中创建的组件。¥the component you created in
App.js
.
文件的其余部分将所有部分组合在一起,并将最终产品注入 public
文件夹中的 index.html
。
¥The remainder of the file brings all the pieces together and injects the final product into index.html
in the public
folder.
构建面板
¥Building the board
让我们回到 App.js
。你将在这里度过本教程的其余部分。
¥Let’s get back to App.js
. This is where you’ll spend the rest of the tutorial.
目前面板只有一个方块,但你需要九个!如果你只是尝试复制粘贴你的方块来制作两个像这样的方块:
¥Currently the board is only a single square, but you need nine! If you just try and copy paste your square to make two squares like this:
export default function Square() {
return <button className="square">X</button><button className="square">X</button>;
}
你会得到这个错误:
¥You’ll get this error:
React 组件需要返回单个 JSX 元素,而不是像两个按钮那样的多个相邻的 JSX 元素。要解决此问题,你可以使用片段(<>
和 </>
)来封装多个相邻的 JSX 元素,如下所示:
¥React components need to return a single JSX element and not multiple adjacent JSX elements like two buttons. To fix this you can use Fragments (<>
and </>
) to wrap multiple adjacent JSX elements like this:
export default function Square() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
现在你应该看到:
¥Now you should see:
很棒!现在你只需要复制粘贴几次来添加九个方块以及…
¥Great! Now you just need to copy-paste a few times to add nine squares and…
不好了!这些方块都在一条直线上,而不是像我们的板子需要的那样在网格中。要解决此问题,你需要使用 div
将方块分组到行中并添加一些 CSS 类。当你这样做的时候,你会给每个方块一个数字,以确保你知道每个方块的显示位置。
¥Oh no! The squares are all in a single line, not in a grid like you need for our board. To fix this you’ll need to group your squares into rows with div
s and add some CSS classes. While you’re at it, you’ll give each square a number to make sure you know where each square is displayed.
在 App.js
文件中,将 Square
组件更新为如下所示:
¥In the App.js
file, update the Square
component to look like this:
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
中定义的 CSS 使用 board-row
的 className
样式化 div。现在你已经将组件分组到带有样式 div
的行中,你就拥有了井字面板:
¥The CSS defined in styles.css
styles the divs with the className
of board-row
. Now that you’ve grouped your components into rows with the styled div
s you have your tic-tac-toe board:
但是你现在有问题了。你名为 Square
的组件实际上不再是方块了。让我们通过将名称更改为 Board
来解决这个问题:
¥But you now have a problem. Your component named Square
, really isn’t a square anymore. Let’s fix that by changing the name to Board
:
export default function Board() {
//...
}
此时你的代码应如下所示:
¥At this point your code should look something like this:
export default function Board() { 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> </> ); }
通过属性传递数据
¥Passing data through props
接下来,当用户单击方块时,你需要将方块的值从空更改为 “X”。到目前为止,根据你构建面板的方式,你需要复制粘贴更新方块的代码九次(每个方块一次)!React 的组件架构允许你创建可重用的组件,而不是复制粘贴,以避免混乱、重复的代码。
¥Next, you’ll want to change the value of a square from empty to “X” when the user clicks on the square. With how you’ve built the board so far you would need to copy-paste the code that updates the square nine times (once for each square you have)! Instead of copy-pasting, React’s component architecture allows you to create a reusable component to avoid messy, duplicated code.
首先,你要将定义第一个方块 (<button className="square">1</button>
) 的行从 Board
组件复制到新的 Square
组件中:
¥First, you are going to copy the line defining your first square (<button className="square">1</button>
) from your Board
component into a new Square
component:
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
然后,你将更新 Board 组件以使用 JSX 语法渲染 Square
组件:
¥Then you’ll update the Board component to render that Square
component using JSX syntax:
// ...
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
必须以大写字母开头。
¥Note how unlike the browser div
s, your own components Board
and Square
must start with a capital letter.
让我们来看看:
¥Let’s take a look:
不好了!你失去了你以前有编号的方块。现在每个方块都写着 “1”。要解决此问题,你将使用属性将每个方块应具有的值从父组件 (Board
) 传递到其子级 (Square
)。
¥Oh no! You lost the numbered squares you had before. Now each square says “1”. To fix this, you will use props to pass the value each square should have from the parent component (Board
) to its child (Square
).
更新 Square
组件以读取你将从 Board
传递的 value
属性:
¥Update the Square
component to read the value
prop that you’ll pass from the Board
:
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
表示 Square 组件可以传递一个名为 value
的属性。
¥function Square({ value })
indicates the Square component can be passed a prop called value
.
现在你想在每个方块内显示 value
而不是 1
。尝试这样做:
¥Now you want to display that value
instead of 1
inside every square. Try doing it like this:
function Square({ value }) {
return <button className="square">value</button>;
}
糟糕,这不是你想要的:
¥Oops, this is not what you wanted:
你想从你的组件中渲染名为 value
的 JavaScript 变量,而不是单词 “value”。从 JSX 到 “转义到 JavaScript”,你需要大括号。在 JSX 中的 value
周围添加大括号,如下所示:
¥You wanted to render the JavaScript variable called value
from your component, not the word “value”. To “escape into JavaScript” from JSX, you need curly braces. Add curly braces around value
in JSX like so:
function Square({ value }) {
return <button className="square">{value}</button>;
}
现在,你应该看到一个空面板:
¥For now, you should see an empty board:
这是因为 Board
组件尚未将 value
属性传递给它渲染的每个 Square
组件。要修复它,你需要将 value
属性添加到由 Board
组件渲染的每个 Square
组件:
¥This is because the Board
component hasn’t passed the value
prop to each Square
component it renders yet. To fix it you’ll add the value
prop to each Square
component rendered by the Board
component:
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>
</>
);
}
现在你应该再次看到数字网格:
¥Now you should see a grid of numbers again:
你更新后的代码应如下所示:
¥Your updated code should look like this:
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> </> ); }
制作一个交互组件
¥Making an interactive component
当你单击它时,让我们用 X
填充 Square
组件。在 Square
中声明一个名为 handleClick
的函数。然后,将 onClick
添加到从 Square
返回的按钮 JSX 元素的属性中:
¥Let’s fill the Square
component with an X
when you click it. Declare a function called handleClick
inside of the Square
. Then, add onClick
to the props of the button JSX element returned from the Square
:
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
如果你现在单击一个方块,你应该会在 CodeSandbox 的浏览器部分底部的控制台选项卡中看到一条日志,上面写着 "clicked!"
。多次单击方块将再次记录 "clicked!"
。具有相同消息的重复控制台日志不会在控制台中创建更多行。而是,你会在第一个 "clicked!"
日志旁边看到一个递增的计数器。
¥If you click on a square now, you should see a log saying "clicked!"
in the Console tab at the bottom of the Browser section in CodeSandbox. Clicking the square more than once will log "clicked!"
again. Repeated console logs with the same message will not create more lines in the console. Instead, you will see an incrementing counter next to your first "clicked!"
log.
下一步,你希望 Square 组件 “记住” 它被单击,并用 “X” 标记填充它。对于 “记住” 事情,组件使用状态。
¥As a next step, you want the Square component to “remember” that it got clicked, and fill it with an “X” mark. To “remember” things, components use state.
React 提供了一个名为 useState
的特殊函数,你可以从你的组件中调用它来让它做 “记住” 事情。让我们把 Square
的当前值存储在状态中,当 Square
被点击时改变它。
¥React provides a special function called useState
that you can call from your component to let it “remember” things. Let’s store the current value of the Square
in state, and change it when the Square
is clicked.
在文件顶部导入 useState
。从 Square
组件中移除 value
属性。而是,在调用 useState
的 Square
的开头添加一个新行。让它返回一个名为 value
的状态变量:
¥Import useState
at the top of the file. Remove the value
prop from the Square
component. Instead, add a new line at the start of the Square
that calls useState
. Have it return a state variable called value
:
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
存储值,setValue
是可用于更改值的函数。传递给 useState
的 null
用作此状态变量的初始值,因此此处的 value
开始等于 null
。
¥value
stores the value and setValue
is a function that can be used to change the value. The null
passed to useState
is used as the initial value for this state variable, so value
here starts off equal to null
.
由于 Square
组件不再接受属性,你将从 Board 组件创建的所有九个 Square 组件中删除 value
属性:
¥Since the Square
component no longer accepts props anymore, you’ll remove the value
prop from all nine of the Square components created by the Board component:
// ...
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>
</>
);
}
现在,你将更改 Square
以在单击时显示 “X”。将 console.log("clicked!");
事件处理程序替换为 setValue('X');
。现在你的 Square
组件看起来像这样:
¥Now you’ll change Square
to display an “X” when clicked. Replace the console.log("clicked!");
event handler with setValue('X');
. Now your Square
component looks like this:
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
通过从 onClick
处理程序调用此 set
函数,你告诉 React 在单击 <button>
时重新渲染 Square
。更新后,Square
的 value
会变成 'X'
,所以你会在游戏面板上看到 “X”。单击任何一个 Square,“X” 应该会出现:
¥By calling this set
function from an onClick
handler, you’re telling React to re-render that Square
whenever its <button>
is clicked. After the update, the Square
’s value
will be 'X'
, so you’ll see the “X” on the game board. Click on any Square, and “X” should show up:
每个 Square 都有自己的状态:每个 Square 中存储的 value
完全独立于其他 Square。当你在组件中调用 set
函数时,React 也会自动更新内部的子组件。
¥Each Square has its own state: the value
stored in each Square is completely independent of the others. When you call a set
function in a component, React automatically updates the child components inside too.
完成上述更改后,你的代码将如下所示:
¥After you’ve made the above changes, your code will look like this:
import { useState } from 'react'; function Square() { const [value, setValue] = useState(null); function handleClick() { setValue('X'); } return ( <button className="square" onClick={handleClick} > {value} </button> ); } 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> </> ); }
React 开发者工具
¥React Developer Tools
React DevTools 让你可以检查 React 组件的属性和状态。你可以在 CodeSandbox 的浏览器部分底部找到 React DevTools 选项卡:
¥React DevTools let you check the props and the state of your React components. You can find the React DevTools tab at the bottom of the browser section in CodeSandbox:
要检查屏幕上的特定组件,请使用 React DevTools 左上角的按钮:
¥To inspect a particular component on the screen, use the button in the top left corner of React DevTools:
完成游戏
¥Completing the game
至此,你已经拥有井字游戏的所有基本构建块。要玩完整的游戏,你现在需要在面板上交替放置 “X” 和 “O”,并且你需要一种确定获胜者的方法。
¥By this point, you have all the basic building blocks for your tic-tac-toe game. To have a complete game, you now need to alternate placing “X”s and “O”s on the board, and you need a way to determine a winner.
提升状态
¥Lifting state up
目前,每个 Square
组件都维护游戏状态的一部分。为了检查井字游戏的赢家,Board
需要以某种方式知道 9 个 Square
组件中每个组件的状态。
¥Currently, each Square
component maintains a part of the game’s state. To check for a winner in a tic-tac-toe game, the Board
would need to somehow know the state of each of the 9 Square
components.
你会如何处理?起初,你可能会猜测 Board
需要 “询问” 每个 Square
才能获得 Square
的状态。尽管这种方法在 React 中在技术上是可行的,但我们不鼓励这样做,因为代码变得难以理解、容易出现错误并且难以重构。而是,最好的方法是将游戏的状态存储在父 Board
组件中,而不是每个 Square
中。Board
组件可以通过传递一个属性来告诉每个 Square
要显示什么,就像你将数字传递给每个 Square 时所做的那样。
¥How would you approach that? At first, you might guess that the Board
needs to “ask” each Square
for that Square
’s state. Although this approach is technically possible in React, we discourage it because the code becomes difficult to understand, susceptible to bugs, and hard to refactor. Instead, the best approach is to store the game’s state in the parent Board
component instead of in each Square
. The Board
component can tell each Square
what to display by passing a prop, like you did when you passed a number to each Square.
要从多个子组件收集数据,或者让两个子组件相互通信,请在其父组件中声明共享状态。父组件可以通过属性将该状态传递回子组件。这使得子组件之间以及与其父组件保持同步。
¥To collect data from multiple children, or to have two child components communicate with each other, declare the shared state in their parent component instead. The parent component can pass that state back down to the children via props. This keeps the child components in sync with each other and with their parent.
重构 React 组件时,将状态提升到父组件中很常见。
¥Lifting state into a parent component is common when React components are refactored.
让我们借此机会尝试一下。编辑 Board
组件,使其声明一个名为 squares
的状态变量,该变量默认为对应于 9 个方块的 9 个空值数组:
¥Let’s take this opportunity to try it out. Edit the Board
component so that it declares a state variable named squares
that defaults to an array of 9 nulls corresponding to the 9 squares:
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
创建一个包含九个元素的数组并将它们中的每一个都设置为 null
。围绕它的 useState()
调用声明了一个最初设置为该数组的 squares
状态变量。数组中的每个条目对应于一个方块的值。当你稍后填充面板时,squares
数组将如下所示:
¥Array(9).fill(null)
creates an array with nine elements and sets each of them to null
. The useState()
call around it declares a squares
state variable that’s initially set to that array. Each entry in the array corresponds to the value of a square. When you fill the board in later, the squares
array will look like this:
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
现在,你的 Board
组件需要将 value
属性传递给它渲染的每个 Square
:
¥Now your Board
component needs to pass the value
prop down to each Square
that it renders:
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
属性。这将需要删除 Square 组件自己的 value
状态跟踪和按钮的 onClick
属性:
¥Next, you’ll edit the Square
component to receive the value
prop from the Board component. This will require removing the Square component’s own stateful tracking of value
and the button’s onClick
prop:
function Square({value}) {
return <button className="square">{value}</button>;
}
此时你应该看到一个空的井字面板:
¥At this point you should see an empty tic-tac-toe board:
你的代码应该是这样的:
¥And your code should look like this:
import { useState } from 'react'; function Square({ value }) { return <button className="square">{value}</button>; } 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 现在都会收到一个 value
属性,对于空方块,它将是 'X'
、'O'
或 null
。
¥Each Square will now receive a value
prop that will either be 'X'
, 'O'
, or null
for empty squares.
接下来,你需要更改单击 Square
时发生的情况。Board
组件现在维护填充了哪些方块。你需要为 Square
创建一种方法来更新 Board
的状态。由于状态对于定义它的组件是私有的,因此你不能直接从 Square
更新 Board
的状态。
¥Next, you need to change what happens when a Square
is clicked. The Board
component now maintains which squares are filled. You’ll need to create a way for the Square
to update the Board
’s state. Since state is private to a component that defines it, you cannot update the Board
’s state directly from Square
.
而是,你会将一个函数从 Board
组件传递到 Square
组件,并且你将让 Square
在单击方块时调用该函数。你将从单击 Square
组件时将调用的函数开始。你将调用该函数 onSquareClick
:
¥Instead, you’ll pass down a function from the Board
component to the Square
component, and you’ll have Square
call that function when a square is clicked. You’ll start with the function that the Square
component will call when it is clicked. You’ll call that function onSquareClick
:
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
接下来,你将 onSquareClick
函数添加到 Square
组件的属性中:
¥Next, you’ll add the onSquareClick
function to the Square
component’s props:
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
现在,你将 onSquareClick
属性连接到 Board
组件中的一个函数,你将命名为 handleClick
。要将 onSquareClick
连接到 handleClick
,你需要将一个函数传递给第一个 Square
组件的 onSquareClick
属性:
¥Now you’ll connect the onSquareClick
prop to a function in the Board
component that you’ll name handleClick
. To connect onSquareClick
to handleClick
you’ll pass a function to the onSquareClick
prop of the first Square
component:
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={handleClick} />
//...
);
}
最后,你将在 Board 组件内定义 handleClick
函数以更新保存你的面板状态的 squares
数组:
¥Lastly, you will define the handleClick
function inside the Board component to update the squares
array holding your board’s state:
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
函数使用 JavaScript slice()
数组方法创建 squares
数组 (nextSquares
) 的副本。然后,handleClick
更新 nextSquares
数组以将 X
添加到第一个([0]
索引)方块。
¥The handleClick
function creates a copy of the squares
array (nextSquares
) with the JavaScript slice()
Array method. Then, handleClick
updates the nextSquares
array to add X
to the first ([0]
index) square.
调用 setSquares
函数让 React 知道组件的状态已经改变。这将触发使用 squares
状态 (Board
) 的组件及其子组件(构成面板的 Square
组件)的重新渲染。
¥Calling the setSquares
function lets React know the state of the component has changed. This will trigger a re-render of the components that use the squares
state (Board
) as well as its child components (the Square
components that make up the board).
现在你可以将 X 添加到面板上…但仅限于左上角的方块。你的 handleClick
函数被硬编码为更新左上角 (0
) 的索引。让我们更新 handleClick
以便能够更新任何方块。将参数 i
添加到 handleClick
函数,该函数采用要更新的方块索引:
¥Now you can add X’s to the board… but only to the upper left square. Your handleClick
function is hardcoded to update the index for the upper left square (0
). Let’s update handleClick
to be able to update any square. Add an argument i
to the handleClick
function that takes the index of the square to update:
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
属性设置为 handleClick(0)
,但这是行不通的:
¥Next, you will need to pass that i
to handleClick
. You could try to set the onSquareClick
prop of square to be handleClick(0)
directly in the JSX like this, but it won’t work:
<Square value={squares[0]} onSquareClick={handleClick(0)} />
这就是为什么这不起作用。handleClick(0)
调用将成为渲染面板组件的一部分。因为 handleClick(0)
通过调用 setSquares
改变了面板组件的状态,所以你的整个面板组件将再次重新渲染。但这再次运行 handleClick(0)
,导致无限循环:
¥Here is why this doesn’t work. The handleClick(0)
call will be a part of rendering the board component. Because handleClick(0)
alters the state of the board component by calling setSquares
, your entire board component will be re-rendered again. But this runs handleClick(0)
again, leading to an infinite loop:
为什么这个问题没有早点发生?
¥Why didn’t this problem happen earlier?
当你传递 onSquareClick={handleClick}
时,你将 handleClick
函数作为属性向下传递。你不是在调用它!但是现在你立即调用该函数 - 注意 handleClick(0)
中的括号 - 这就是它运行得太早的原因。在用户点击之前,你不想调用 handleClick
!
¥When you were passing onSquareClick={handleClick}
, you were passing the handleClick
function down as a prop. You were not calling it! But now you are calling that function right away—notice the parentheses in handleClick(0)
—and that’s why it runs too early. You don’t want to call handleClick
until the user clicks!
你可以通过创建一个调用 handleClick(0)
的函数(如 handleFirstSquareClick
)、一个调用 handleClick(1)
的函数(如 handleSecondSquareClick
)等来解决此问题。你可以像 onSquareClick={handleFirstSquareClick}
一样将这些函数作为属性传递(而不是调用)。这将解决无限循环。
¥You could fix this by creating a function like handleFirstSquareClick
that calls handleClick(0)
, a function like handleSecondSquareClick
that calls handleClick(1)
, and so on. You would pass (rather than call) these functions down as props like onSquareClick={handleFirstSquareClick}
. This would solve the infinite loop.
但是,定义九个不同的函数并为每个函数命名太冗长了。而是,让我们这样做:
¥However, defining nine different functions and giving each of them a name is too verbose. Instead, let’s do this:
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
请注意新的 () =>
语法。这里,() => handleClick(0)
是一个箭头函数,它是定义函数的一种更短的方式。当点击方块时,会运行 =>
“箭头” 之后的代码,调用 handleClick(0)
。
¥Notice the new () =>
syntax. Here, () => handleClick(0)
is an arrow function, which is a shorter way to define functions. When the square is clicked, the code after the =>
“arrow” will run, calling handleClick(0)
.
现在你需要更新其他八个方块以从你传递的箭头函数中调用 handleClick
。确保 handleClick
的每次调用的参数对应于正确方块的索引:
¥Now you need to update the other eight squares to call handleClick
from the arrow functions you pass. Make sure that the argument for each call of the handleClick
corresponds to the index of the correct 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>
</>
);
};
现在你可以再次通过单击将 X 添加到面板上的任何方块:
¥Now you can again add X’s to any square on the board by clicking on them:
但是这次所有的状态管理都由 Board
组件处理!
¥But this time all the state management is handled by the Board
component!
这是你的代码应该是这样的:
¥This is what your code should look like:
import { useState } from 'react'; function Square({ value, onSquareClick }) { return ( <button className="square" onClick={onSquareClick}> {value} </button> ); } 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 ( <> <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> </> ); }
现在你的状态处理在 Board
组件中,父 Board
组件将属性传递给子 Square
组件,以便它们可以正确显示。单击 Square
时,子 Square
组件现在要求父 Board
组件更新面板的状态。当 Board
的状态改变时,Board
组件和每个子 Square
都会自动重新渲染。保持 Board
组件中所有方块的状态将允许它确定未来的获胜者。
¥Now that your state handling is in the Board
component, the parent Board
component passes props to the child Square
components so that they can be displayed correctly. When clicking on a Square
, the child Square
component now asks the parent Board
component to update the state of the board. When the Board
’s state changes, both the Board
component and every child Square
re-renders automatically. Keeping the state of all squares in the Board
component will allow it to determine the winner in the future.
让我们回顾一下当用户单击你的面板左上角的方块以向其添加 X
时会发生什么:
¥Let’s recap what happens when a user clicks the top left square on your board to add an X
to it:
-
单击左上角的方块运行
button
从Square
作为其onClick
属性接收的函数。Square
组件从Board
接收该函数作为其onSquareClick
属性。Board
组件直接在 JSX 中定义了该函数。它以参数0
调用handleClick
。¥Clicking on the upper left square runs the function that the
button
received as itsonClick
prop from theSquare
. TheSquare
component received that function as itsonSquareClick
prop from theBoard
. TheBoard
component defined that function directly in the JSX. It callshandleClick
with an argument of0
. -
handleClick
使用参数 (0
) 将squares
数组的第一个元素从null
更新为X
。¥
handleClick
uses the argument (0
) to update the first element of thesquares
array fromnull
toX
. -
Board
组件的squares
状态已更新,因此Board
及其所有子级都将重新渲染。这会导致索引为0
的Square
组件的value
属性从null
更改为X
。¥The
squares
state of theBoard
component was updated, so theBoard
and all of its children re-render. This causes thevalue
prop of theSquare
component with index0
to change fromnull
toX
.
最后,用户看到左上角的方块在点击后从空变为有一个 X
。
¥In the end the user sees that the upper left square has changed from empty to having a X
after clicking it.
为什么不可变性很重要
¥Why immutability is important
请注意,在 handleClick
中,你如何调用 .slice()
来创建 squares
数组的副本,而不是修改现有数组。为了解释原因,我们需要讨论不可变性以及为什么学习不可变性很重要。
¥Note how in handleClick
, you call .slice()
to create a copy of the squares
array instead of modifying the existing array. To explain why, we need to discuss immutability and why immutability is important to learn.
通常有两种更改数据的方法。第一种方法是通过直接更改数据的值来改变数据。第二种方法是用具有所需更改的新副本替换数据。如果你改变 squares
数组,它会是这样的:
¥There are generally two approaches to changing data. The first approach is to mutate the data by directly changing the data’s values. The second approach is to replace the data with a new copy which has the desired changes. Here is what it would look like if you mutated the squares
array:
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
数组的情况下更改数据,它会是这样的:
¥And here is what it would look like if you changed data without mutating the squares
array:
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`
结果是一样的,但通过不直接改变(改变底层数据),你可以获得几个好处。
¥The result is the same but by not mutating (changing the underlying data) directly, you gain several benefits.
不可变性使复杂的功能更容易实现。在本教程的后面部分,你将实现一个 “时光旅行” 功能,让你可以查看游戏的历史和 “跳回” 到过去的动作。此功能并非特定于游戏 - 撤消和重做某些操作的能力是应用的常见要求。避免直接数据突变可以让你保持以前版本的数据完好无损,并在以后重用它们。
¥Immutability makes complex features much easier to implement. Later in this tutorial, you will implement a “time travel” feature that lets you review the game’s history and “jump back” to past moves. This functionality isn’t specific to games—an ability to undo and redo certain actions is a common requirement for apps. Avoiding direct data mutation lets you keep previous versions of the data intact, and reuse them later.
不可变性还有另一个好处。默认情况下,当父组件的状态发生变化时,所有子组件都会自动重新渲染。这甚至包括未受更改影响的子组件。尽管重新渲染本身不会引起用户注意(你不应该主动尝试避免它!),但出于性能原因,你可能希望跳过重新渲染显然不受其影响的树的一部分。不可变性使得组件比较其数据是否已更改的成本非常低。你可以在 memo
API 参考 中了解有关 React 如何选择何时重新渲染组件的更多信息。
¥There is also another benefit of immutability. By default, all child components re-render automatically when the state of a parent component changes. This includes even the child components that weren’t affected by the change. Although re-rendering is not by itself noticeable to the user (you shouldn’t actively try to avoid it!), you might want to skip re-rendering a part of the tree that clearly wasn’t affected by it for performance reasons. Immutability makes it very cheap for components to compare whether their data has changed or not. You can learn more about how React chooses when to re-render a component in the memo
API reference.
轮流
¥Taking turns
现在是时候修复这个井字游戏的一个主要缺陷了:“O” 不能在面板上标记。
¥It’s now time to fix a major defect in this tic-tac-toe game: the “O”s cannot be marked on the board.
默认情况下,你会将第一步设置为 “X”。让我们通过向 Board 组件添加另一个状态来跟踪这一点:
¥You’ll set the first move to be “X” by default. Let’s keep track of this by adding another piece of state to the Board component:
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
每次玩家移动时,xIsNext
(布尔值)将被翻转以确定下一个玩家,游戏状态将被保存。你将更新 Board
的 handleClick
函数以翻转 xIsNext
的值:
¥Each time a player moves, xIsNext
(a boolean) will be flipped to determine which player goes next and the game’s state will be saved. You’ll update the Board
’s handleClick
function to flip the value of 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 (
//...
);
}
现在,当你点击不同的方块时,它们将在 X
和 O
之间交替,这是它们应该做的!
¥Now, as you click on different squares, they will alternate between X
and O
, as they should!
但是,等等,有一个问题。尝试多次点击同一个方块:
¥But wait, there’s a problem. Try clicking on the same square multiple times:
X
被 O
覆盖了!虽然这会给游戏带来非常有趣的变化,但我们现在将坚持原来的规则。
¥The X
is overwritten by an O
! While this would add a very interesting twist to the game, we’re going to stick to the original rules for now.
当你用 X
或 O
标记一个方块时,你并不是首先检查该方块是否已经具有 X
或 O
值。你可以通过提早返回来解决此问题。你将检查该方块是否已经有 X
或 O
。如果方块已经填满,你将在 handleClick
函数中提前执行 return
- 在它尝试更新面板状态之前。
¥When you mark a square with a X
or an O
you aren’t first checking to see if the square already has a X
or O
value. You can fix this by returning early. You’ll check to see if the square already has a X
or an O
. If the square is already filled, you will return
in the handleClick
function early—before it tries to update the board state.
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
现在你只能将 X
或 O
添加到空方块中!此时你的代码应该如下所示:
¥Now you can only add X
’s or O
’s to empty squares! Here is what your code should look like at this point:
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> </> ); }
宣布获胜者
¥Declaring a winner
现在玩家可以轮流玩了,你需要显示游戏何时获胜并且没有更多轮流可玩。为此,你将添加一个名为 calculateWinner
的辅助函数,它采用 9 个方块的数组,检查获胜者并根据需要返回 'X'
、'O'
或 null
。不要太担心 calculateWinner
函数;它不是 React 特有的:
¥Now that the players can take turns, you’ll want to show when the game is won and there are no more turns to make. To do this you’ll add a helper function called calculateWinner
that takes an array of 9 squares, checks for a winner and returns 'X'
, 'O'
, or null
as appropriate. Don’t worry too much about the calculateWinner
function; it’s not specific to 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
函数中调用 calculateWinner(squares)
来检查玩家是否获胜。你可以在检查用户是否单击了已经具有 X
或 O
的方块的同时执行此检查。在这两种情况下,我们都希望尽早返回:
¥You will call calculateWinner(squares)
in the Board
component’s handleClick
function to check if a player has won. You can perform this check at the same time you check if a user has clicked a square that already has a X
or and O
. We’d like to return early in both cases:
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
为了让玩家知道游戏何时结束,你可以显示诸如“获胜者:X”or” 获胜者:O”.为此,你需要将 status
部分添加到 Board
组件。如果游戏结束,状态将显示获胜者,如果游戏正在进行,你将显示下一个轮到哪个玩家:
¥To let the players know when the game is over, you can display text such as “Winner: X” or “Winner: O”. To do that you’ll add a status
section to the Board
component. The status will display the winner if the game is over and if the game is ongoing you’ll display which player’s turn is next:
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">
// ...
)
}
恭喜!你现在有一个可以运行的井字游戏。你也刚刚学习了 React 的基础知识。所以你是这里真正的赢家。代码应该如下所示:
¥Congratulations! You now have a working tic-tac-toe game. And you’ve just learned the basics of React too. So you are the real winner here. Here is what the code should look like:
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; }
添加时光旅行
¥Adding time travel
作为最后的练习,让我们可以 “时光倒流” 到游戏中的先前动作。
¥As a final exercise, let’s make it possible to “go back in time” to the previous moves in the game.
存储移动历史
¥Storing a history of moves
如果你改变了 squares
数组,时光旅行将变得非常困难。
¥If you mutated the squares
array, implementing time travel would be very difficult.
但是,你在每次移动后使用 slice()
创建 squares
数组的新副本,并将其视为不可变的。这将允许你存储 squares
数组的每个过去版本,并在已经发生的轮次之间导航。
¥However, you used slice()
to create a new copy of the squares
array after every move, and treated it as immutable. This will allow you to store every past version of the squares
array, and navigate between the turns that have already happened.
你将把过去的 squares
数组存储在另一个名为 history
的数组中,你将把它存储为一个新的状态变量。history
数组表示所有面板状态,从第一步到最后一步,其形状如下:
¥You’ll store the past squares
arrays in another array called history
, which you’ll store as a new state variable. The history
array represents all board states, from the first to the last move, and has a shape like this:
[
// 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'],
// ...
]
再次提升状态
¥Lifting state up, again
你现在将编写一个名为 Game
的新顶层组件来显示过去的移动列表。你将在此处放置包含整个游戏历史的 history
状态。
¥You will now write a new top-level component called Game
to display a list of past moves. That’s where you will place the history
state that contains the entire game history.
将 history
状态放入 Game
组件将使你可以从其子 Board
组件中删除 squares
状态。就像你从 Square
组件 “提升状态” 到 Board
组件一样,你现在将它从 Board
提升到顶层 Game
组件。这使 Game
组件可以完全控制 Board
的数据,并让它指示 Board
渲染 history
的先前回合。
¥Placing the history
state into the Game
component will let you remove the squares
state from its child Board
component. Just like you “lifted state up” from the Square
component into the Board
component, you will now lift it up from the Board
into the top-level Game
component. This gives the Game
component full control over the Board
’s data and lets it instruct the Board
to render previous turns from the history
.
首先,添加一个带有 export default
的 Game
组件。让它渲染 Board
组件和一些标记:
¥First, add a Game
component with export default
. Have it render the Board
component and some markup:
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
关键字,并将它们添加到 function Game() {
声明之前。这会告诉你的 index.js
文件使用 Game
组件而不是 Board
组件作为顶层组件。Game
组件返回的额外 div
正在为你稍后添加到面板的游戏信息腾出空间。
¥Note that you are removing the export default
keywords before the function Board() {
declaration and adding them before the function Game() {
declaration. This tells your index.js
file to use the Game
component as the top-level component instead of your Board
component. The additional div
s returned by the Game
component are making room for the game information you’ll add to the board later.
向 Game
组件添加一些状态以跟踪下一个玩家和移动历史:
¥Add some state to the Game
component to track which player is next and the history of moves:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
// ...
请注意 [Array(9).fill(null)]
是一个包含单个条目的数组,它本身是一个包含 9 个 null
的数组。
¥Notice how [Array(9).fill(null)]
is an array with a single item, which itself is an array of 9 null
s.
要渲染当前移动的方块,你需要从 history
中读取最后一个方块数组。为此你不需要 useState
- 你已经有足够的信息在渲染过程中计算它:
¥To render the squares for the current move, you’ll want to read the last squares array from the history
. You don’t need useState
for this—you already have enough information to calculate it during rendering:
export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];
// ...
接下来,在 Game
组件内创建一个 handlePlay
函数,Board
组件将调用该函数来更新游戏。将 xIsNext
、currentSquares
和 handlePlay
作为属性传递给 Board
组件:
¥Next, create a handlePlay
function inside the Game
component that will be called by the Board
component to update the game. Pass xIsNext
, currentSquares
and handlePlay
as props to the Board
component:
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
组件完全由它接收到的属性控制。更改 Board
组件以获取三个属性:xIsNext
、squares
和一个新的 onPlay
函数,Board
可以在玩家移动时使用更新的方块数组调用这些函数。接下来,删除调用 useState
的 Board
函数的前两行:
¥Let’s make the Board
component fully controlled by the props it receives. Change the Board
component to take three props: xIsNext
, squares
, and a new onPlay
function that Board
can call with the updated squares array when a player makes a move. Next, remove the first two lines of the Board
function that call useState
:
function Board({ xIsNext, squares, onPlay }) {
function handleClick(i) {
//...
}
// ...
}
现在,将 Board
组件中 handleClick
中的 setSquares
和 setXIsNext
调用替换为对新 onPlay
函数的单个调用,以便 Game
组件可以在用户单击方块时更新 Board
:
¥Now replace the setSquares
and setXIsNext
calls in handleClick
in the Board
component with a single call to your new onPlay
function so the Game
component can update the Board
when the user clicks a square:
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
组件传递给它的属性控制。你需要在 Game
组件中实现 handlePlay
函数才能让游戏重新运行。
¥The Board
component is fully controlled by the props passed to it by the Game
component. You need to implement the handlePlay
function in the Game
component to get the game working again.
handlePlay
被调用时应该做什么?请记住,Board 曾经使用更新的数组调用 setSquares
;现在它将更新后的 squares
数组传递给 onPlay
。
¥What should handlePlay
do when called? Remember that Board used to call setSquares
with an updated array; now it passes the updated squares
array to onPlay
.
handlePlay
函数需要更新 Game
的状态以触发重新渲染,但你没有可以再调用的 setSquares
函数 - 你现在正在使用 history
状态变量来存储此信息。你需要通过将更新的 squares
数组附加为新的历史条目来更新 history
。你还想切换 xIsNext
,就像 Board 过去所做的那样:
¥The handlePlay
function needs to update Game
’s state to trigger a re-render, but you don’t have a setSquares
function that you can call any more—you’re now using the history
state variable to store this information. You’ll want to update history
by appending the updated squares
array as a new history entry. You also want to toggle xIsNext
, just as Board used to do:
export default function Game() {
//...
function handlePlay(nextSquares) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
//...
}
在这里,[...history, nextSquares]
创建了一个新数组,其中包含 history
中的所有条目,然后是 nextSquares
。(你可以将 ...history
展开语法 读作“枚举 history
中的所有项目”。)
¥Here, [...history, nextSquares]
creates a new array that contains all the items in history
, followed by nextSquares
. (You can read the ...history
spread syntax as “enumerate all the items in history
”.)
例如,如果 history
为 [[null,null,null], ["X",null,null]]
,nextSquares
为 ["X",null,"O"]
,则新的 [...history, nextSquares]
数组将为 [[null,null,null], ["X",null,null], ["X",null,"O"]]
。
¥For example, if history
is [[null,null,null], ["X",null,null]]
and nextSquares
is ["X",null,"O"]
, then the new [...history, nextSquares]
array will be [[null,null,null], ["X",null,null], ["X",null,"O"]]
.
此时,你已将状态移动到 Game
组件中,UI 应该完全正常工作,就像重构之前一样。这是此时代码的样子:
¥At this point, you’ve moved the state to live in the Game
component, and the UI should be fully working, just as it was before the refactor. Here is what the code should look like at this point:
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; }
显示过去的动作
¥Showing the past moves
由于你正在记录井字游戏的历史,因此你现在可以向玩家显示过去的动作列表。
¥Since you are recording the tic-tac-toe game’s history, you can now display a list of past moves to the player.
像 <button>
这样的 React 元素是常规的 JavaScript 对象;你可以在你的应用中传递它们。要在 React 中渲染多个条目,你可以使用 React 元素数组。
¥React elements like <button>
are regular JavaScript objects; you can pass them around in your application. To render multiple items in React, you can use an array of React elements.
你已经有一个状态为 history
动作的数组,所以现在你需要将其转换为一个 React 元素数组。在 JavaScript 中,要将一个数组转换为另一个数组,你可以使用 数组 map
方法:
¥You already have an array of history
moves in state, so now you need to transform it to an array of React elements. In JavaScript, to transform one array into another, you can use the array map
method:
[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
你将使用 map
将你的 history
动作转换为代表屏幕上按钮的 React 元素,并显示按钮列表以 “跳转” 到过去的动作。让我们在游戏组件中对 history
进行 map
:
¥You’ll use map
to transform your history
of moves into React elements representing buttons on the screen, and display a list of buttons to “jump” to past moves. Let’s map
over the history
in the Game component:
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>
);
}
你可以在下面看到你的代码应该是什么样子。请注意,你应该会在开发者工具控制台中看到一条错误消息:
¥You can see what your code should look like below. Note that you should see an error in the developer tools console that says:
你将在下一节中修复此错误。
¥You’ll fix this error in the next section.
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); } 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> ); } 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; }
当你在传递给 map
的函数中遍历 history
数组时,squares
参数遍历 history
的每个元素,move
参数遍历每个数组索引:0
, 1
, 2
, ….(在大多数情况下,你需要实际的数组元素,但要渲染移动列表,你只需要索引。)
¥As you iterate through history
array inside the function you passed to map
, the squares
argument goes through each element of history
, and the move
argument goes through each array index: 0
, 1
, 2
, …. (In most cases, you’d need the actual array elements, but to render a list of moves you will only need indexes.)
对于井字游戏历史中的每一步,你创建一个包含按钮 <button>
的列表项 <li>
。该按钮有一个 onClick
处理程序,它调用一个名为 jumpTo
的函数(你尚未实现)。
¥For each move in the tic-tac-toe game’s history, you create a list item <li>
which contains a button <button>
. The button has an onClick
handler which calls a function called jumpTo
(that you haven’t implemented yet).
现在,你应该会看到游戏中发生的动作列表和开发者工具控制台中的错误。让我们讨论一下 “键” 错误的含义。
¥For now, you should see a list of the moves that occurred in the game and an error in the developer tools console. Let’s discuss what the “key” error means.
选择一个键
¥Picking a key
当你渲染一个列表时,React 会存储一些关于每个渲染列表项的信息。当你更新一个列表时,React 需要确定发生了什么变化。你可以添加、删除、重新排列或更新列表的条目。
¥When you render a list, React stores some information about each rendered list item. When you update a list, React needs to determine what has changed. You could have added, removed, re-arranged, or updated the list’s items.
想象一下从
¥Imagine transitioning from
<li>Alexa: 7 tasks left</li>
<li>Ben: 5 tasks left</li>
到
¥to
<li>Ben: 9 tasks left</li>
<li>Claudia: 8 tasks left</li>
<li>Alexa: 5 tasks left</li>
除了更新的计数之外,阅读本文的人可能会说你交换了 Alexa 和 Ben 的顺序,并在 Alexa 和 Ben 之间插入了 Claudia。然而,React 是一个计算机程序,不知道你的意图,因此你需要为每个列表项指定一个键属性,以区分每个列表项与其同级列表项。如果你的数据来自数据库,Alexa、Ben 和 Claudia 的数据库 ID 可以用作键。
¥In addition to the updated counts, a human reading this would probably say that you swapped Alexa and Ben’s ordering and inserted Claudia between Alexa and Ben. However, React is a computer program and does not know what you intended, so you need to specify a key property for each list item to differentiate each list item from its siblings. If your data was from a database, Alexa, Ben, and Claudia’s database IDs could be used as keys.
<li key={user.id}>
{user.name}: {user.taskCount} tasks left
</li>
重新渲染列表时,React 获取每个列表项的键并搜索前一个列表的项以查找匹配的键。如果当前列表有一个之前不存在的键,React 会创建一个组件。如果当前列表缺少前一个列表中存在的键,React 会销毁前一个组件。如果两个键匹配,则移动相应的组件。
¥When a list is re-rendered, React takes each list item’s key and searches the previous list’s items for a matching key. If the current list has a key that didn’t exist before, React creates a component. If the current list is missing a key that existed in the previous list, React destroys the previous component. If two keys match, the corresponding component is moved.
键告诉 React 每个组件的身份,这允许 React 在重新渲染之间保持状态。如果组件的键发生变化,组件将被销毁并以新状态重新创建。
¥Keys tell React about the identity of each component, which allows React to maintain state between re-renders. If a component’s key changes, the component will be destroyed and re-created with a new state.
key
是 React 中一个特殊的预留属性。创建元素时,React 提取 key
属性并将键直接存储在返回的元素上。尽管 key
看起来像是作为属性传递的,但 React 会自动使用 key
来决定要更新哪些组件。组件无法询问其父级指定的 key
。
¥key
is a special and reserved property in React. When an element is created, React extracts the key
property and stores the key directly on the returned element. Even though key
may look like it is passed as props, React automatically uses key
to decide which components to update. There’s no way for a component to ask what key
its parent specified.
强烈建议你在构建动态列表时分配适当的键。如果你没有合适的键,你可能需要考虑重组你的数据,以便你这样做。
¥It’s strongly recommended that you assign proper keys whenever you build dynamic lists. If you don’t have an appropriate key, you may want to consider restructuring your data so that you do.
如果没有指定键,React 会报错,默认使用数组索引作为键。在尝试重新排序列表项或插入/删除列表项时,使用数组索引作为键是有问题的。显式传递 key={i}
会消除错误,但会出现与数组索引相同的问题,因此在大多数情况下不推荐使用。
¥If no key is specified, React will report an error and use the array index as a key by default. Using the array index as a key is problematic when trying to re-order a list’s items or inserting/removing list items. Explicitly passing key={i}
silences the error but has the same problems as array indices and is not recommended in most cases.
键不需要是全局唯一的;它们只需要在组件及其同级组件之间是唯一的。
¥Keys do not need to be globally unique; they only need to be unique between components and their siblings.
实现时光旅行
¥Implementing time travel
在井字游戏的历史中,每个过去的动作都有一个与之关联的唯一 ID:这是移动的序号。移动永远不会被重新排序、删除或插入中间,因此使用移动索引作为键是安全的。
¥In the tic-tac-toe game’s history, each past move has a unique ID associated with it: it’s the sequential number of the move. Moves will never be re-ordered, deleted, or inserted in the middle, so it’s safe to use the move index as a key.
在 Game
函数中,你可以将键添加为 <li key={move}>
,如果你重新加载渲染的游戏,React 的 “键” 错误应该会消失:
¥In the Game
function, you can add the key as <li key={move}>
, and if you reload the rendered game, React’s “key” error should disappear:
const moves = history.map((squares, move) => {
//...
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{description}</button>
</li>
);
});
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); } 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 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; }
在你可以实现 jumpTo
之前,你需要 Game
组件来跟踪用户当前正在查看的步骤。为此,定义一个名为 currentMove
的新状态变量,默认为 0
:
¥Before you can implement jumpTo
, you need the Game
component to keep track of which step the user is currently viewing. To do this, define a new state variable called currentMove
, defaulting to 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
函数以更新 currentMove
。如果你将 currentMove
更改为偶数,你还可以将 xIsNext
设置为 true
。
¥Next, update the jumpTo
function inside Game
to update that currentMove
. You’ll also set xIsNext
to true
if the number that you’re changing currentMove
to is even.
export default function Game() {
// ...
function jumpTo(nextMove) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}
//...
}
你现在将对 Game
的 handlePlay
函数进行两处更改,当你单击一个方块时将调用该函数。
¥You will now make two changes to the Game
’s handlePlay
function which is called when you click on a square.
-
如果你 “时光倒流” 然后从那个点开始一个新的动作,你只想保持那个点的历史。不是在
history
中的所有条目(...
扩展语法)之后添加nextSquares
,而是在history.slice(0, currentMove + 1)
中的所有条目之后添加它,这样你就只保留旧历史的那部分。¥If you “go back in time” and then make a new move from that point, you only want to keep the history up to that point. Instead of adding
nextSquares
after all items (...
spread syntax) inhistory
, you’ll add it after all items inhistory.slice(0, currentMove + 1)
so that you’re only keeping that portion of the old history. -
每次移动时,你都需要更新
currentMove
以指向最新的历史条目。¥Each time a move is made, you need to update
currentMove
to point to the latest history entry.
function handlePlay(nextSquares) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}
最后,你将修改 Game
组件以渲染当前选定的动作,而不是始终渲染最后的动作:
¥Finally, you will modify the Game
component to render the currently selected move, instead of always rendering the final move:
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];
// ...
}
如果你单击游戏历史记录中的任何一步,井字面板应立即更新以显示面板在该步骤发生后的样子。
¥If you click on any step in the game’s history, the tic-tac-toe board should immediately update to show what the board looked like after that step occurred.
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 [currentMove, setCurrentMove] = useState(0); const currentSquares = history[currentMove]; function handlePlay(nextSquares) { const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]; setHistory(nextHistory); setCurrentMove(nextHistory.length - 1); setXIsNext(!xIsNext); } function jumpTo(nextMove) { setCurrentMove(nextMove); setXIsNext(nextMove % 2 === 0); } 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; }
最后清理
¥Final cleanup
如果仔细查看代码,你可能会注意到当 currentMove
为偶数时为 xIsNext === true
,而当 currentMove
为奇数时为 xIsNext === false
。换句话说,如果你知道 currentMove
的值,那么你总能算出 xIsNext
应该是什么。
¥If you look at the code very closely, you may notice that xIsNext === true
when currentMove
is even and xIsNext === false
when currentMove
is odd. In other words, if you know the value of currentMove
, then you can always figure out what xIsNext
should be.
你没有理由将这两者都存储在状态中。事实上,总是尽量避免冗余状态。简化你在状态中存储的内容可以减少错误并使你的代码更易于理解。更改 Game
,使其不将 xIsNext
存储为单独的状态变量,而是根据 currentMove
计算出来:
¥There’s no reason for you to store both of these in state. In fact, always try to avoid redundant state. Simplifying what you store in state reduces bugs and makes your code easier to understand. Change Game
so that it doesn’t store xIsNext
as a separate state variable and instead figures it out based on the currentMove
:
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
状态声明或对 setXIsNext
的调用。现在,xIsNext
没有机会与 currentMove
不同步,即使你在编写组件代码时出错也是如此。
¥You no longer need the xIsNext
state declaration or the calls to setXIsNext
. Now, there’s no chance for xIsNext
to get out of sync with currentMove
, even if you make a mistake while coding the components.
封装起来
¥Wrapping up
恭喜!你已经创建了一个井字游戏:
¥Congratulations! You’ve created a tic-tac-toe game that:
-
让你玩井字游戏,
¥Lets you play tic-tac-toe,
-
指示玩家何时赢得游戏,
¥Indicates when a player has won the game,
-
随着游戏的进行存储游戏的历史,
¥Stores a game’s history as a game progresses,
-
允许玩家回顾游戏的历史并查看游戏面板的以前版本。
¥Allows players to review a game’s history and see previous versions of a game’s board.
干得好!我们希望你现在觉得你对 React 的工作原理有了很好的了解。
¥Nice work! We hope you now feel like you have a decent grasp of how React works.
在这里查看最终结果:
¥Check out the final result here:
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; }
如果你有额外的时间或想练习新的 React 技能,这里有一些你可以改进井字游戏的想法,按难度递增的顺序列出:
¥If you have extra time or want to practice your new React skills, here are some ideas for improvements that you could make to the tic-tac-toe game, listed in order of increasing difficulty:
-
仅对于当前移动,显示 “你在移动 #…” 而不是按钮。
¥For the current move only, show “You are at move #…” instead of a button.
-
重写
Board
以使用两个循环来制作方块而不是对它们进行硬编码。¥Rewrite
Board
to use two loops to make the squares instead of hardcoding them. -
添加一个切换按钮,让你可以按升序或降序对移动进行排序。
¥Add a toggle button that lets you sort the moves in either ascending or descending order.
-
当有人获胜时,高亮导致获胜的三个方块(当没有人获胜时,显示一条关于结果为平局的消息)。
¥When someone wins, highlight the three squares that caused the win (and when no one wins, display a message about the result being a draw).
-
在移动历史列表中以 (row, col) 格式显示每个移动的位置。
¥Display the location for each move in the format (row, col) in the move history list.
在本教程中,你已经接触了 React 概念,包括元素、组件、属性和状态。现在你已经了解了这些概念在构建游戏时如何工作,请查看 在 React 中思考 以了解相同的 React 概念在构建应用的 UI 时如何工作。
¥Throughout this tutorial, you’ve touched on React concepts including elements, components, props, and state. Now that you’ve seen how these concepts work when building a game, check out Thinking in React to see how the same React concepts work when building an app’s UI.