React|Quick Start

基础声明

创建和嵌套组件

React 组件是返回标签的 JavaScript 函数

1
2
3
4
5
function MyButton(){
return (
<button>测试按钮</button>
);
}

定义了这个组件之后,我们可以将其嵌套在另一个组件的声明

1
2
3
4
5
6
7
8
export default function App() {
return (
<div>
<h1>测试应用</h1>
<MyButton/>
</div>
);
}

其中上面的 export default 定义了文件中的重要组件

同时组件内引入组件的时候,标签名称必须是大写开头,React中组件名称都要求大写开头

export 声明用于从 JavaScript 模块中导出值。导出的值可通过import声明或动态导入来将其导入其他程序

JSX

上面所使用的通过JS函数返回HTML标签的语法被称为 JSX

JSX 元素是 JavaScript 代码和 HTML 标签的组合,用于描述要显示的内容

JSX 比 HTML 更加严格。你必须闭合标签,如 <br />

可以看到我们上面定义标签的时候,在h1和自己定义的组件里,外部还套了一层div,这是因为组件也不允许一次直接返回多个 JSX 标签。我们必须将它们包裹到一个共享的父级中,比如 <div>...</div> 或使用空的 <>...</> 包裹

所以上述的代码也可以调整为这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function TestButton() {
return (
<button>测试按钮</button>
);
}

export default function App(){
return (
<>
<h1>测试应用</h1>
<TestButton />
</>
);
}

添加样式

React 中我们通过 className 指定一个 CSS 的class,等价于 html 中指定 class 属性

React并未约定如何引入css,后续学习到的框架或者构建工具会实现

显示数据

JSX的作用就是把HTML写入js内,渲染页面基本元素

而React支持在标签内写{}的方式又可以再回到js代码的编写,实现了页面数据的动态展示

1
2
3
4
5
6
7
8
9
10
11
const user = {
name: 'Test'
};

function showData() {
retrun (
<h1>
{user.name}
</h1>
);
}

上述就将标题内容展示为指定数据 user.name 也就是 Test

一个更为复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//定义数据
const user = {
name: 'Test',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
imageSize: 100,
};

export default function Profile() {
return (
<>
<h1>{user.name}</h1>
<img
className = "avatar"
src = {user.imageUrl}
alt = {'Photo of' + user.name}
style = {{
width: user.imageSize,
weight: user.imageSize
}}
/>
</>
);
}

需要注意上述的 style = {{}} 并非特殊语法,最外层的大括号表示我们要在JSX中嵌入JS数据,而内层的大括号是一个js中的对象,由于style的属性接收的都是kv键值对,因此内层还需要一对括号定义JS的对象。

所以在上面的代码中,我们先通过花括号定义了一个key分别是width和weight的js对象,然后再套了一对花括号来在JSX中嵌入JS对象。

所以上述的两对并不是react中特殊的语法,理论上html标签中属性支持对象的元素在JSX中进行自定义初始化操作的声明 都需要两对花括号。

条件展示/指定属性

这部分React并未进行特殊约定,因此我们可以直接写js的if-else代码,条件引入JSX,之后通过{}的方式将JSX以JS元素的方式渲染返回

1
2
3
4
5
6
7
8
9
10
11
let content;
if(isLogin) {
content = <AdminPage />
} else {
content = <LoginForm />
}
return (
<>
{content}
</>
);

当然也可以使用三元运算符,不过需要注意的是三元运算符需要工作在 JSX 内部

1
2
3
4
5
6
7
<div>
{isLogin ? (
<AdminPage />
) : (
<LoginForm />
)}
</div>

列表数据展示

使用JSX标签语法下的<li>,同时使用JS提供的 map() 来处理映射每一个列表数据元素

假设我们有一组列表数据

1
2
3
4
5
const products = [
{ title: '卷心菜', isFruit: false, id: 1 },
{ title: '大蒜', isFruit: false, id: 2 },
{ title: '苹果', isFruit: true, id: 3 },
];

现在需要展示为列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function ShoppingList() {
const listItems = products.map(product =>
<li
key = {product.id}
style = {{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
}

上述 listItem 元素中 key 属性用来唯一区分列表元素,一般从后端数据里需要给出

在这里我们只是用 map 函数将数据根据字段修改了样式,还是将js逻辑修改应用到标签的样式,因此使用style属性

map函数返回了修改样式后的数据数组,直接用ul进行展示

响应事件

1
2
3
4
5
6
7
8
9
10
11
12
function ClickableButton (){
//点击事件处理函数
function handleClick() {
alert("clicked");
}
return (
//不直接调用事件处理函数,而是传递给事件
<button onClick = {handleClick}>
点击
</button>
);
}

useState

(1)首先需要从React中引入

1
import { useState } from 'react';

(2)在组件中声明 state

1
2
3
function CountButton() {
const [count, setCount] = useState(0);
}

(3)调用 useState 可以获得当前的 state 以及用于更新他的函数,命名随意,但是一般我们都用 const [xxx, setXXX] = useState() 来声明 state

(4)默认值:上面我们给 useState 函数的入参传入了0,这个作用是设置变量的默认值

(5)每个组件内部声明 useState 获得到的变量是独立的,也就是说我们引入两个 <CountButton> 可以得到两个独立统计点击次数的按钮

点击按钮累计计数的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useState } from 'react';

export default function MyApp() {
return (
<>
<h1>独立更新的计数器</h1>
<CountButton/>
<CountButton/>
</>
);
}

function CountButton () {
const [count, setCount] = useState(0);
function handleClick () {
setCount(count + 1);
}
return (
<button onClick = {handleClick}>
Click {count} times
</button>
);
}

Hook

use 开头的函数官方称之为 Hook,上面 useState 就是 React 内置的一个 Hook

React API docs

Hooks 是React中一个很重要的概念,后续会重点进行学习

Hook 比普通函数更为严格。只能在你的组件(或其他 Hook)的 顶层 调用 Hook

如果想在一个条件或循环中使用 useState,需要提取一个新的组件并在组件内部使用它

组件间数据共享

上述点击按钮展示点击次数的demo,多个按钮的useState的数据是独立的

我们想要在多个组件间数据共享,例如点击一次,同步更新到两个按钮显示数据里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { useState } from 'react';
export default function ShareButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<>
<h1>共同更新的按钮</h1>
<CustomizeButton count = {count} handleClick = {handleClick}/>
<CustomizeButton count = {count} handleClick = {handleClick}/>
</>
);
}

function CustomizeButton({count, handleClick}) {
return (
<button onClick = {handleClick}>
Click {count} times
</button>
);
}

核心在于我们将 useState 的声明放在了父级组件,之后将hook中声明的state作为组件的参数传递到所有的子组件的prop上

function CustomizeButton({count, handleClick}) 这里就是定义了两个prop:count和handleClick

这种方式称为状态提升

工程基本结构

教程:井字棋游戏 – React 中文文档

以井字棋游戏为例,项目基本结构:

  • public/index.html 最终解析的目录页面
  • src/index.js 应用和浏览器之间的桥梁
  • src/style.css 样式文件

其中 index.js 内的头几行非常关键

1
2
3
4
5
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';

import App from './App';

分别是:

  • React 库
  • React 与 Web 浏览器对话的库(React DOM)
  • 组件的样式
  • App.js 里面创建的组件

井字棋demo

同个组件内显式调用函数的死循环

在井字棋实现过程中,遇到了死循环的现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState } from "react";

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 showValue={squares[0]} onSquareClick={handleClick(0)} />
<Square showValue={squares[1]} onSquareClick={handleClick(1)}/>
<Square showValue={squares[2]} onSquareClick={handleClick(2)}/>
</div>
<div className="board-row">
<Square showValue={squares[3]} onSquareClick={handleClick(3)}/>
<Square showValue={squares[4]} onSquareClick={handleClick(4)}/>
<Square showValue={squares[5]} onSquareClick={handleClick(5)}/>
</div>
<div className="board-row">
<Square showValue={squares[6]} onSquareClick={handleClick(6)}/>
<Square showValue={squares[7]} onSquareClick={handleClick(7)}/>
<Square showValue={squares[8]} onSquareClick={handleClick(8)}/>
</div>
</>
);
}

function Square({ showValue, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{showValue}
</button>
);
}

这是因为 onClickHandle={handleClick(3)} 这种方式写,由于都在 Board 组件内声明,会直接调用Board组件中的handleClick进行setSquares重新渲染,然后由于重新渲染了Board,return时又会调用Board组件中的handleClick进行setSquares重新渲染,因此导致了无限循环

解决方案是套一层,我们可以定义9个不同的函数,分别调用handleClick传入0-8的index,但是也可以直接用箭头函数快速声明匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { useState } from "react";

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 showValue={squares[0]} onSquareClick={() => handleClick(0)} />
<Square showValue={squares[1]} onSquareClick={() => handleClick(1)} />
<Square showValue={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square showValue={squares[3]} onSquareClick={() => handleClick(3)} />
<Square showValue={squares[4]} onSquareClick={() => handleClick(4)} />
<Square showValue={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square showValue={squares[6]} onSquareClick={() => handleClick(6)} />
<Square showValue={squares[7]} onSquareClick={() => handleClick(7)} />
<Square showValue={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

function Square({ showValue, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{showValue}
</button>
);
}

命名规范

在 React 中,我们通常使用 onSomething 命名事件的props,用handleSomething 命名处理事件的函数

例如上面,点击棋盘的事件我们命名为 onSquareClick,点击后更新棋盘的事件处理函数我们命名为 handleClick

核心实现思路

  • 基础组件绘制:编写 jsx & 引入 props
  • 实现落子:使用 props & state
  • 实现交替落子:在合适的位置定义额外的 state
  • 校验一个格子只能落一次子:基础逻辑完善
  • 计算胜者:简单算法 & 整体流程把握(应该在什么时候计算胜者?)

基础组件

  • 棋盘组件——多个方格组件
  • 方格组件——按钮

落子

本质就是更改棋盘数组的数据

父级棋盘需要一个 state 来维护所有的格子的状态

当点击方格的时候,子组件(方格)需要能更新棋盘维护的格子的状态,采用 prop 的方式传递父组件(棋盘)的更新棋盘逻辑到 button 的 onClick 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
//更新棋盘逻辑,更新指定方格
function handleClick(i) {
//保证数据不变性,直接copy一份副本
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares);
}
return (
<>
<div className="board-row">
<Square showValue={squares[0]} onSquareClick={() => handleClick(0)} />
<Square showValue={squares[1]} onSquareClick={() => handleClick(1)} />
<Square showValue={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square showValue={squares[3]} onSquareClick={() => handleClick(3)} />
<Square showValue={squares[4]} onSquareClick={() => handleClick(4)} />
<Square showValue={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square showValue={squares[6]} onSquareClick={() => handleClick(6)} />
<Square showValue={squares[7]} onSquareClick={() => handleClick(7)} />
<Square showValue={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);

function Square({ showValue, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{showValue}
</button>
);
}

交替落子

  • useState,由于只会有两种子,选择true/false
  • 每次一个人下完之后,更新true/false同时更新落的子的符号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
//再引入一个state,记录落子先后
const [xIsNext, setXIsNext] = useState(true);
function handleClick(i) {
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
//更新落子顺序
setXIsNext(!xIsNext);
}
return (
<>
//...
</>
);

function Square({ showValue, onSquareClick }) {
return (
//...
);
}

一个格子只允许落一次

在点击的处理函数逻辑中补充优先判断

如果已经有value了,直接返回不做任何处理

1
2
3
4
5
6
7
8
9
10
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
function handleClick(i) {
//放在最前面
if(squares[i]){
return;
}
//...
}

计算胜者

每一次渲染棋盘的时候(注意不是点击按钮)都优先计算是否出现了胜者

如果出现了胜者,返回获胜的是X还是O

一个简单的算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function calculateWinner(board) {
//枚举所有的获胜判断条件
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 index = 0; index < lines.length; index++) {
const [a, b, c] = lines[index];
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
//返回获胜的棋子
return board[a];
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
function handleClick(i) {
//...
}
//每一次渲染的时候都计算获胜者
const winner = calculateWinner(squares);
let msg;
if (winner) {
msg = "Winner is:" + winner;
} else {
msg = "Next player is: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{msg}</div>
//...
</>
);
}

点击的棋盘之后:优先判断,是否出现了胜者,已经出现了直接return,同时如果已经在一个位置下下棋子就不允许在更新棋盘,也是直接return

1
2
3
4
5
6
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
//...
}

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { useState } from "react";

export default function Board() {
const [squares, setSquares] = useState(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);
function handleClick(i) {
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
const winner = calculateWinner(squares);
let msg;
if (winner) {
msg = "Winner is:" + winner;
} else {
msg = "Next player is: " + (xIsNext ? "X" : "O");
}
return (
<>
<div className="status">{msg}</div>
<div className="board-row">
<Square showValue={squares[0]} onSquareClick={() => handleClick(0)} />
<Square showValue={squares[1]} onSquareClick={() => handleClick(1)} />
<Square showValue={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square showValue={squares[3]} onSquareClick={() => handleClick(3)} />
<Square showValue={squares[4]} onSquareClick={() => handleClick(4)} />
<Square showValue={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square showValue={squares[6]} onSquareClick={() => handleClick(6)} />
<Square showValue={squares[7]} onSquareClick={() => handleClick(7)} />
<Square showValue={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}

function Square({ showValue, onSquareClick }) {
return (
<button className="square" onClick={onSquareClick}>
{showValue}
</button>
);
}

function calculateWinner(board) {
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 index = 0; index < lines.length; index++) {
const [a, b, c] = lines[index];
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
return null;
}


React|Quick Start
http://example.com/2025/01/23/React-Quick-Start/
作者
Noctis64
发布于
2025年1月23日
许可协议