初识React
什么是React
React是一个用于构建用户界面的JavaScript库,由Facebook开发和维护。它允许开发者通过组合简单的组件来构建复杂的用户界面。
为什么选择React
- 组件化:React的设计思想是将UI分解成独立的、可复用的组件。
- 高效:React使用虚拟DOM来减少直接操作DOM的次数,从而提高性能。
- 社区支持:React有一个庞大的开发者社区和丰富的生态系统。
开始你的第一个React项目
我们可以利用vite 来创建一个react项目(Vite 需要 Node.js 版本 18+,20+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本。)
sql
npm create vite@latest
用你的编译器打开项目,并且在终端执行命令安装依赖,再用npm run dev 来启动项目
arduino
npm intall
npm run dev
打开链接可以看到网页
然后删除App.css和App.jsx的内容,准备编写我们自己的项目
在编写之前了解一下js与jsx文件
特点 | JS 文件 | JSX 文件 |
---|---|---|
扩展名 | .js |
.jsx |
语法 | 纯 JavaScript | JavaScript + JSX |
用途 | 通用 JavaScript 逻辑 | React 组件的 UI 结构 |
是否需要编译 | 通常不需要(除非使用 ES6+) | 需要编译成纯 JavaScript |
支持 HTML 样式 | 不支持 | 支持(通过 JSX 语法) |
开始
App.jsx
将App.jsx里的文件修改为:
javascript
export default function App() {
return <div></div>;
}
App.jsx
返回了一个空组件App,后续我们会在这里加入更多我们写的组件
注意,这里的样式并不是React自带的,所以我们需要自己定义并引入,具体见下
index.css
该文件定义了 React 应用的大体样式。
为和官方教程样式一致,将index.css内容先替换为
css
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}
h1 {
margin-top: 0;
font-size: 22px;
}
h2 {
margin-top: 0;
font-size: 20px;
}
h3 {
margin-top: 0;
font-size: 18px;
}
h4 {
margin-top: 0;
font-size: 16px;
}
h5 {
margin-top: 0;
font-size: 14px;
}
h6 {
margin-top: 0;
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-inline-start: 20px;
}
main.jsx
在本教程中我们不会编辑此文件,但它是 App.js
文件中创建的组件与 Web 浏览器之间的桥梁。
javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
第 1-4 行将所有必要的部分组合在一起:
- React
- React 与 Web 浏览器对话的库(React DOM)
- 组件的样式
App.js
里面创建的组件
其他文件将它们组合在一起,并将最终成果注入 public
文件夹里面的 index.html
中。
构建棋盘
首先,创建一个存放组件的文件夹,再创建一个Board文件夹,存放Board.jsx和样式文件board.css
先在Board.jsx中创建一个方块组件
javascript
export default function Board() {
return <button className="square">X</button>;
}
在board.css中添加
css
.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}
在 React 中,组件是一段可重用代码,它通常作为 UI 界面的一部分。组件用于渲染、管理和更新应用中的 UI 元素。让我们逐行查看这段代码,看看发生了什么:
1、第一行定义了一个名为 Board
的函数。JavaScript 的 export
关键字使此函数可以在此文件之外访问。default
关键字表明它是文件中的主要函数。
2、第二行返回一个按钮。JavaScript 的 return
关键字意味着后面的内容都作为值返回给函数的调用者。<button>
是一个 JSX 元素。JSX 元素是 JavaScript 代码和 HTML 标签的组合,用于描述要显示的内容。className="square"
是一个 button 属性,它决定 CSS 如何设置按钮的样式。X
是按钮内显示的文本,</button>
闭合 JSX 元素以表示不应将任何后续内容放置在按钮内。
为了让该组件可以展示在网页中,我们需要将Board组件导出,并在App组件中导入
javascript
import Board from "./components/Board/Board.jsx";
export default function App() {
return <Board/>;
}
浏览器会像下面这样在方块里面显示一个 X:
目前棋盘只有一个方块,但你需要九个!如果你只是想着复制粘贴来制作两个像这样的方块:
javascript
export default function Board() {
return <button className="square">X</button><button className="square">X</button>;
}
你将会得到如下错误:
D:\Desktop\tictactoe\src\components\Board\Board.jsx: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>
?
React 组件必须返回单个 JSX 元素,不能像两个按钮那样返回多个相邻的 JSX 元素。要解决此问题,可以使用 Fragment(<>
与 </>
)包裹多个相邻的 JSX 元素,如下所示:
javascript
export default function Board() {
return (
<>
<button className="square">X</button>
<button className="square">X</button>
</>
);
}
现在你应该可以看见:
非常棒!现在你只需要通过复制粘贴来添加九个方块,然后......
但事与愿违的是这些方块并没有排列成网格,而是都在一条线上。要解决此问题,需要使用 div
将方块分到每一行中并添加一些 CSS 样式。当你这样做的时候,需要给每个方块一个数字,以确保你知道每个方块的位置。
css
.board-row:after {
clear: both;
content: '';
display: table;
}
App.js
文件中,Square
组件看起来像这样:
javascript
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>
</>
);
}
借助 board.css
中定义的 board-row
样式,我们将组件分到每一行的 div
中。最终完成了井字棋棋盘:
通过 props 传递数据
接下来,当用户单击方块时,我们要将方块的值从空更改为"X"。根据目前构建的棋盘,你需要复制并粘贴九次更新方块的代码(每个方块都需要一次)!但是,React 的组件架构可以创建可重用的组件,以避免混乱、重复的代码。
首先,要将定义第一个方块(<button className="square">1</button>
)的这行代码从 Board
组件复制到新的 Square
组件中:
javascript
function Square() {
return <button className="square">1</button>;
}
export default function Board() {
// ...
}
然后,更新 Board 组件并使用 JSX 语法渲染 Square
组件:
javascript
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
,必须以大写字母开头。
让我们来看一看效果:
哦不!你失去了你以前有正确编号的方块。现在每个方块都写着"1"。要解决此问题,需要使用 props 将每个方块应有的值从父组件(Board
)传递到其子组件(Square
)。
更新 Square
组件,读取从 Board
传递的 value
props:
javascript
function Square({ value }) {
return <button className="square">1</button>;
}
function Square({ value })
表示可以向 Square 组件传递一个名为 value
的 props。
现在你如果想要显示对应的 value
而不是 1
,可以试一下像下面这样:
javascript
function Square({ value }) {
return <button className="square">value</button>;
}
糟糕!这还不是你想要的:
我们需要从组件中渲染名为 value
的 JavaScript 变量,而不是"value"这个词。要从 JSX"转义到 JavaScript",你需要使用大括号。在 JSX 中的 value
周围添加大括号,如下所示:
javascript
function Square({ value }) {
return <button className="square">{value}</button>;
}
现在,你应该会看到一个空的棋盘了:
这是因为 Board
组件还没有将 value
props 传递给它渲染的每个 Square
组件。要修复这个问题,需要向 Board
组件里面的每个 Square
组件添加 value
props:
ini
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>
</>
);
}
现在你应该能再次看到数字网格:
创建一个具有交互性的组件
当你单击它的时候,Square
组件需要显示"X"。在 Square
内部声明一个名为 handleClick
的函数。然后,将 onClick
添加到由 Square
返回的 JSX 元素的 button 的 props 中:
javascript
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
打开开发者工具的控制台,如果现在单击一个方块,你应该会看到一条日志,上面写着 "clicked!"
。多次单击方块将再次记录 "clicked!"
。具有相同消息的重复控制台日志不会在控制台中重复创建。而你会在第一次 "clicked!"
旁边看到一个递增的计数器。
下一步,我们希望 Square 组件能够"记住"它被单击过,并用"X"填充它。为了"记住"一些东西,组件使用 state。
React 提供了一个名为 useState
的特殊函数,可以从组件中调用它来让它"记住"一些东西。让我们将 Square
的当前值存储在 state 中,并在单击 Square
时更改它。
在文件的顶部导入 useState
。从 Square
组件中移除 value
props。在调用 useState
的 Square
的开头添加一个新行。让它返回一个名为 value 的 state 变量:
javascript
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
//...
value
存储值,而 setValue
是可用于更改值的函数。传递给 useState
的 null
用作这个 state 变量的初始值,因此此处 value
的值开始时等于 null
。
由于 Square
组件不再接受 props,我们从 Board 组件创建的所有九个 Square 组件中删除 value
props:
javascript
// ...
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
组件看起来像这样:
javascript
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
通过从 onClick
处理程序调用此 set
函数,你告诉 React
在单击其 <button>
时要重新渲染该 Square
。更新后,方块的值将为"X",因此会在棋盘上看到"X"。点击任意方块,"X"应该出现
每个 Square 都有自己的 state:存储在每个 Square 中的 value
完全独立于其他的 Square。当你在组件中调用 set 函数时,React 也会自动更新内部的子组件。
完成这个游戏
至此,你已经拥有井字棋游戏的所有基本构建块。要玩完整的游戏,你现在需要在棋盘上交替放置"X"和"O",并且你需要一种确定获胜者的方法。
状态提升
目前,每个 Square
组件都维护着游戏 state 的一部分。要检查井字棋游戏中的赢家,Board
需要以某种方式知道 9 个 Square
组件中每个组件的 state。
你会如何处理?起初,你可能会猜测 Board
需要向每个 Square
"询问"Square
的 state。尽管这种方法在 React 中在技术上是可行的,但我们不鼓励这样做,因为代码变得难以理解、容易出现错误并且难以重构。相反,最好的方法是将游戏的 state 存储在 Board
父组件中,而不是每个 Square
中。Board
组件可以通过传递一个 props 来告诉每个 Square
显示什么,就像你将数字传递给每个 Square 时所做的那样。
要从多个子组件收集数据,或让两个子组件相互通信,请改为在其父组件中声明共享 state。父组件可以通过 props 将该 state 传回给子组件。这使子组件彼此同步并与其父组件保持同步。
重构 React 组件时,将状态提升到父组件中很常见。
让我们借此机会尝试一下。编辑 Board
组件,使其声明一个名为 squares
的 state 变量,该变量默认为对应于 9 个方块的 9 个空值数组:
javascript
// ...
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
return (
// ...
);
}
Array(9).fill(null)
创建了一个包含九个元素的数组,并将它们中的每一个都设置为 null
。包裹它的 useState()
声明了一个初始设置为该数组的 squares
state 变量。数组中的每个元素对应于一个 square 的值。当你稍后填写棋盘时,squares
数组将如下所示:
csharp
['O', null, 'X', 'X', 'X', 'O', 'O', null, null]
现在你的 Board
组件需要将 value
props 向下传递给它渲染的每个 Square
:
javascript
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:
javascript
function Square({value}) {
return <button className="square">{value}</button>;
}
此时你应该看到一个空的井字棋棋盘:
现在,每个 Square 都会收到一个 value
props,对于空方块,该 props 将是 'X'
、'O'
或 null
。
接下来,你需要更改单击 Square
时发生的情况。Board
组件现在维护已经填充过的方块。你需要为 Square
创建一种更新 Board
state 的方法。由于 state 对于定义它的组件是私有的,因此你不能直接从 Square
更新 Board
的 state。
你将从 Board
组件向下传递一个函数到 Square
组件,然后让 Square
在单击方块时调用该函数。我们将从单击 Square
组件时将调用的函数开始。调用该函数 onSquareClick
:
javascript
function Square({ value }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
接下来,将 onSquareClick
函数添加到 Square
组件的 props 中:
javascript
function Square({ value, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
现在,你将把 onSquareClick
props 连接到 Board
组件中的一个函数,命名为 handleClick
。要将 onSquareClick
连接到 handleClick
,需要将一个函数传递给第一个 Square
组件的 onSquareClick
props:
ini
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
函数来更新并保存棋盘 state 的 squares
数组:
scss
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]
索引)方块。
调用 setSquares
函数让 React 知道组件的 state 已经改变。这将触发使用 squares
state 的组件(Board
)及其子组件(构成棋盘的 Square
组件)的重新渲染。
注意
JavaScript 支持闭包,这意味着内部函数(例如 handleClick
)可以访问外部函数(例如 Board
)中定义的变量和函数。handleClick
函数可以读取 squares
state 并调用 setSquares
方法,因为它们都是在 Board
函数内部定义的。
现在你可以将 X 添加到棋盘上......但只能添加到左上角的方块。你的 handleClick
函数被硬编码为更新左上角方块( 0
)的索引。让我们更新 handleClick
以便能够更新任何方块。将参数 i
添加到 handleClick
函数,该函数采用要更新的 square 索引:
scss
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 设置为 handleClick(0)
,但这是行不通的:
scss
<Square value={squares[0]} onSquareClick={handleClick(0)} />
为什么会是这样呢?handleClick(0)
调用将成为渲染 Board 组件的一部分。因为 handleClick(0)
通过调用 setSquares
改变了棋盘组件的 state,所以你的整个棋盘组件将再次重新渲染。但这再次运行了 handleClick(0)
,导致无限循环:
为什么这个问题没有早点发生?
当你传递 onSquareClick={handleClick}
时,你将 handleClick
函数作为 props 向下传递。你不是在调用它!但是现在你立即调用了该函数------注意 handleClick(0)
中的括号------这就是它运行得太早的原因。你不想在用户点击之前调用 handleClick
!
你可以通过创建调用 handleClick(0)
的函数(如 handleFirstSquareClick
)、调用 handleClick(1)
的函数(如 handleSecondSquareClick
)等来修复。你可以将这些函数作为 onSquareClick={handleFirstSquareClick}
之类的 props 传递(而不是调用)。这将解决无限循环的问题。
但是,定义九个不同的函数并为每个函数命名过于冗余。让我们这样做:
scss
export default function Board() {
// ...
return (
<>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
// ...
);
}
注意新的 () =>
语法。这里,() => handleClick(0)
是一个箭头函数,它是定义函数的一种较短的方式。单击方块时,=>
"箭头"之后的代码将运行,调用 handleClick(0)
。
现在你需要更新其他八个方块以从你传递的箭头函数中调用 handleClick
。确保 handleClick
的每次调用的参数对应于正确的 square 索引:
scss
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 添加到棋盘的方块上: 但是这次所有的 state 管理都由 Board
组件处理!
现在,我们在 Board
组件中处理 state, Board
父组件将 props 传递给 Square
子组件,以便它们可以正确显示。单击 Square
时, Square
子组件现在要求 Board
父组件更新棋盘的 state。当 Board
的 state 改变时,Board
组件和每个子 Square
都会自动重新渲染。保存 Board
组件中所有方块的 state 将使得它可以确定未来的赢家。
让我们回顾一下当用户单击你的棋盘左上角的方块以向其添加 X
时会发生什么:
- 单击左上角的方块运行
button
从Square
接收到的onClick
props 的函数。Square
组件从Board
通过onSquareClick
props 接收到该函数。Board
组件直接在 JSX 中定义了该函数。它使用参数0
调用handleClick
。 handleClick
使用参数(0
)将squares
数组的第一个元素从null
更新为X
。Board
组件的squares
state 已更新,因此Board
及其所有子组件都将重新渲染。这会导致索引为0
的Square
组件的value
props 从null
更改为X
。
最后,用户看到左上角的方块在单击后从空变为 X
。
注意
DOM <button>
元素的 onClick
props 对 React 有特殊意义,因为它是一个内置组件。对于像 Square 这样的自定义组件,命名由你决定。你可以给 Square
的 onSquareClick
props 或 Board
的 handleClick
函数起任何名字,代码还是可以运行的。在 React 中,通常使用 onSomething
命名代表事件的 props,使用 handleSomething
命名处理这些事件的函数。
为什么不变性很重要
请注意在 handleClick
中,你调用了 .slice()
来创建 squares
数组的副本而不是修改现有数组。为了解释原因,我们需要讨论不变性以及为什么学习不变性很重要。
通常有两种更改数据的方法。第一种方法是通过直接更改数据的值来改变数据。第二种方法是使用具有所需变化的新副本替换数据。如果你改变 squares
数组,它会是这样的:
csharp
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
数组的情况下更改数据,它会是这样的:
csharp
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`
结果是一样的,但通过不直接改变(改变底层数据),你可以获得几个好处。
不变性使复杂的功能更容易实现。在本教程的后面,你将实现一个"时间旅行"功能,让你回顾游戏的历史并"跳回"到过去的动作。此功能并非特定于游戏------撤消和重做某些操作的能力是应用程序的常见要求。避免数据直接突变可以让你保持以前版本的数据完好无损,并在以后重用它们。
不变性还有另一个好处。默认情况下,当父组件的 state 发生变化时,所有子组件都会自动重新渲染。这甚至包括未受变化影响的子组件。尽管重新渲染本身不会引起用户注意(你不应该主动尝试避免它!),但出于性能原因,你可能希望跳过重新渲染显然不受其影响的树的一部分。不变性使得组件比较其数据是否已更改的成本非常低。你可以在 memo
API 参考 中了解更多关于 React 如何选择何时重新渲染组件的信息。
交替落子
现在是时候修复这个井字棋游戏的一个主要缺陷了:棋盘上无法标记"O"。
默认情况下,你会将第一步设置为"X"。让我们通过向 Board 组件添加另一个 state 来跟踪这一点:
scss
function Board() {
const [xIsNext, setXIsNext] = useState(true);
const [squares, setSquares] = useState(Array(9).fill(null));
// ...
}
每次玩家落子时,xIsNext
(一个布尔值)将被翻转以确定下一个玩家,游戏 state 将被保存。你将更新 Board
的 handleClick
函数以翻转 xIsNext
的值:
scss
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
之间交替,这是它们应该做的!
但是等等,有一个问题。尝试多次点击同一个方块:
X
被 O
覆盖!虽然这会给游戏带来非常有趣的变化,但我们现在将坚持原来的规则。
当你用 X
或 O
标记方块时,你没有检查该方块是否已经具有 X
或 O
值。你可以通过提早返回来解决此问题。我们将检查方块是否已经有 X
或 O
。如果方块已经填满,你将尽早在 handleClick
函数中 return
------在它尝试更新棋盘 state 之前。
ini
function handleClick(i) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
//...
}
现在你只能将 X
或 O
添加到空方块中!
宣布获胜者
现在你可以轮流对战了,接下来我们应该显示游戏何时获胜。为此,你将添加一个名为 calculateWinner
的辅助函数,它接受 9 个方块的数组,检查获胜者并根据需要返回 'X'
、'O'
或 null
。不要太担心 calculateWinner
函数;它不是 React 才会有的:
csharp
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
的方块的同时执行此检查。在这两种情况下,我们都希望尽早返回:
ini
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
//...
}
为了让玩家知道游戏何时结束,你可以显示"获胜者:X"或"获胜者:O"等文字。为此,你需要将 status
部分添加到 Board
组件。如果游戏结束,将显示获胜者,如果游戏正在进行,你将显示下一轮将会是哪个玩家:
ini
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">
// ...
)
}
css
.status {
margin-bottom: 10px;
}
恭喜!你现在有一个可以运行的井字棋游戏。你也学习了 React 的基础知识。所以你是这里真正的赢家。代码应该如下所示:
ini
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;
}
添加时间旅行
其实就是使用state保存状态,同学们自己动手尝试一下
如果做不出来就参照教程:井字棋游戏 -- React 中文文档
结束
相信通过这个教程你对React的使用有了一定的了解,但是学习React你还需要学习更多
可以从看官方文档开始,React 官方中文文档,逐步学习每个概念
加油加油加油!!!