基础声明
创建和嵌套组件
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) { 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)); 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; }
|