目录
[1. 创建和嵌套组件](#1. 创建和嵌套组件)
[2. 标签语言](#2. 标签语言)
[3. 组件用法举例](#3. 组件用法举例)
[3.1 双括号](#3.1 双括号)
[3.2 数组转列表](#3.2 数组转列表)
[3.3 界面交互-事件处理函数](#3.3 界面交互-事件处理函数)
[3.4 界面更新-组件状态和更新函数](#3.4 界面更新-组件状态和更新函数)
[3.5 组件共享状态](#3.5 组件共享状态)
[1. 文件结构](#1. 文件结构)
[2. 项目-井字棋游戏](#2. 项目-井字棋游戏)
[2.1 设计目标](#2.1 设计目标)
[2.2 步骤一:创建基础组件Board](#2.2 步骤一:创建基础组件Board)
[2.3 步骤二:创建嵌套、可复用组件Square](#2.3 步骤二:创建嵌套、可复用组件Square)
[2.4 步骤三:设计可交互、具有状态的组件Square](#2.4 步骤三:设计可交互、具有状态的组件Square)
[2.5 步骤四:重构组件-状态共享](#2.5 步骤四:重构组件-状态共享)
[2.6 步骤五:增加交替落子](#2.6 步骤五:增加交替落子)
[2.7 步骤六:宣布获胜者](#2.7 步骤六:宣布获胜者)
[2.8 步骤七:增加时间旅行并显示落子历史](#2.8 步骤七:增加时间旅行并显示落子历史)
[2.9 步骤八:最后清理](#2.9 步骤八:最后清理)
一、React的定义
React是一个前端Javascript工具库,基于UI(用户)组件构建用户界面。
二、功能
1. 创建和嵌套组件
React 应用程序是由 组件 组成的。一个组件是 UI(用户界面)的一部分,它拥有自己的逻辑和外观。组件可以小到一个按钮,也可以大到整个页面。
javascript
//这种返回标签的javascript函数就是组件
function MyButton() {
return (
<button>我是一个按钮</button>
);
}
//嵌套组件,<MyButton />这种大写字母开头的标签就是react标签
export default function MyApp() { //默认import无需指定名
return (
<div>
<h1>欢迎来到我的应用</h1>
<MyButton />
</div>
);
}
2. 标签语言
使用JSX而不是HTML,JSX类似更严格的HTML,必须闭合标签,也必须有共享父级
javascript
function AboutPage() {
return (
<>
<h1>关于</h1>
<p>你好。<br />最近怎么样?</p>
</>
);
}
如果你有大量的 HTML 需要移植到 JSX 中,你可以使用 在线转换器。
3. 组件用法举例
3.1 双括号
注意双括号的用法,第一个表javascript引用,第二个表对象
javascript
export default function Profile() {
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
}
3.2 数组转列表
数组渲染为列表,常用map,注意li的属性key
javascript
const products = [
{ title: '卷心菜', isFruit: false, id: 1 },
{ title: '大蒜', isFruit: false, id: 2 },
{ title: '苹果', isFruit: true, id: 3 },
];
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>
);
}
3.3 界面交互-事件处理函数
响应事件:可以在组件里定义事件处理函数
javascript
unction MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
点我
</button>
);
}
3.4 界面更新-组件状态和更新函数
更新界面:导入组件状态 和 更新函数
javascript
import { useState } from 'react';
export default function MyApp() {
return (
<div>
<h1>独立更新的计数器</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
点了 {count} 次
</button>
);
}
使用hook:以use开头的函数就是hook,如刚刚的usestate
3.5 组件共享状态
组件间共享数据 :1.状态放在公共组件app;2.mybutton共享状态count和事件处理函数;3. 子组件传入父组件的prop。这种被传递的state和函数被称为prop。
javascript
import { useState } from 'react';
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>共同更新的计数器</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
点了 {count} 次
</button>
);
}
三、实践
1. 文件结构
App.js:创建jsx组件
index.js
:导入react、react-dom、css样式和app.js中的组件,一同注入public-index.html文件
styles.css
:描绘网页样式
public文件夹
2. 项目-井字棋游戏
2.1 设计目标

2.2 步骤一:创建基础组件Board
创建Board组件为九宫格形式

javascript
//app.js
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>
</>
);
}
2.3 步骤二:创建嵌套、可复用组件Square

javascript
// app.js
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>
</>
);
}
2.4 步骤三:设计可交互、具有状态的组件Square

- 在square中增加事件响应函数
Option + ⌘ + J(在 macOS 上)查看控制台

javascript
function Square({ value }) {
function handleClick() {
console.log('clicked!');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
- 界面更新:导入useState,增加square的组件状态和更新函数,使它每次点击更新"X"
javascript
import { useState } from 'react';
function Square() {
const [value, setValue] = useState(null);
function handleClick() {
setValue('X');
}
return (
<button
className="square"
onClick={handleClick}
>
{value}
</button>
);
}
Board也随之改变
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>
</>
);
}
安装拓展后,在网页F12打开开发者工具,可以查看组件的props和state

至此,基本构件块已完成。
2.5 步骤四:重构组件-状态共享
但是在上面的情况,为了决出游戏胜负,Board
需要以某种方式知道 9 个 Square
组件中每个组件的 state。Board
一个个询问 Square显然太复杂,因此
最好的方法是将游戏的 state 存储在 Board
父组件中,而不是每个 Square
中。
重构 React 组件时,将状态提升到父组件中很常见。
-
状态提升到父组件
-
父组件向下传递props
-
子组件接受prop
javascript
import { useState } from 'react';
function Square({ value, onSquareClick }) {
return (
//3. 子组件接受prop
<button className="square" onClick={onSquareClick}>
{value}
</button>
);
}
export default function Board() {
//1. 状态上升到父组件
const [squares, setSquares] = useState(Array(9).fill(null));
//复用事件响应函数
function handleClick(i) {
const nextSquares = squares.slice();
nextSquares[i] = 'X';
setSquares(nextSquares);
}
//2. 父组件传递props给子组件
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>
</>
);
}
为什么 需要const nextSquares = squares.slice();来创建数组的副本而不是直接修改?
- "不直接改变底层数据"允许撤销回顾历史
2.6 步骤五:增加交替落子
向 Board 组件添加另一个 state:xIsNext来跟踪这一点,即默认情况下,你会将第一步设置为"X"。
在事件响应函数中,通过xIsNext来选择x或o,且每次点击翻转一次xIsNext状态
通过检查value,排除单个格子反复点击变化的错误
javascript
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>
</>
);
}
2.7 步骤六:宣布获胜者
需要建立一个函数calculateWinner来判断胜者,它接受 9 个方块的数组,检查获胜者并根据需要返回 'X'
、'O'
或 null。
javascript
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;
}
2.8 步骤七:增加时间旅行并显示落子历史
正如刚才说的,如果你改变了 squares
数组,实现时间旅行将非常困难。
- 把过去的
squares
数组存储在另一个名为history
的数组中,把它存储为一个新的 state 变量。 - 同时,新增一个新的最顶级组件Game,让它渲染board和历史history。
history通过setHistory([...history, nextSquares]);扩展,并需要转化成列表显示,即
javascript
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>
);
其中jumpTo表示一个按钮列表中每个按钮的函数,下一节我们实现。
完整代码如下: 最后,虽然页面成功显示,但是有报错如下:
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `Game`.
解决方法:增加key,key 告诉 React 每个组件的身份,这使得 React 可以在重新渲染时保持 state。如果组件的 key 发生变化,组件将被销毁,新 state 将重新创建。
- 因此,我们将落子索引move作为key
javascript
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;
}
- Game新增状态来记住当前步骤。
const [currentMove, setCurrentMove] = useState(0);
如果你点击游戏历史中的任何一步,井字棋棋盘应立即更新以显示该步骤发生后棋盘的样子。
javascript
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;
}
2.9 步骤八:最后清理
如果你知道 currentMove
的值,那么你总能算出 xIsNext
应该是什么,因此没必要两者都存储成state。更改 Game
使其不将 xIsNext
存储为单独的 state 变量,而是根据 currentMove
计算出来。
javascript
const [history, setHistory] = useState([Array(9).fill(null)]);
const [currentMove, setCurrentMove] = useState(0);
const xIsNext = currentMove % 2 === 0;
const currentSquares = history[currentMove];