一、脚手架搭建项目
基于vite搭建的react的项目
其中react模块和react-dom模块有什么区别
react模块:核心功能、组件react-dom模块:操作浏览器的dom,分为客户端react-dom/client和服务端react-dom/server
二、JSX
概念:JSX是JavaScript语法拓展,可以让你在js文件中书写html标签
JSX语法与HTML语法的写法区别
- 标签要小写
- 标签要闭合
- 不能用
class和for关键字,className和htmlFor(一般用于lable下拉框中匹配)代替 - 属性驼峰式命名
- JSX通过大括号使用
JavaScript - 属性使用大括号
- 唯一根元素
- 添加注释的区别
三、css部分
3.1 react中书写css的方式
在react中,有三种方式书写css样式
- 行内样式
- 全局样式
- 局部样式
import './styles/全局样式.css'
// 局部样式,引入时需要命名
import style from './styles/局部样式.module.css'
function App() {
const myStyle = { color: 'gray' }
return (
<>
{/* 行内样式 */}
<h1 style={myStyle}>呵呵</h1>
<h1 style={{ color: 'red' }}>哈哈</h1>
{/* 全局样式 */}
<div className='box'></div>
{/* 局部样式 */}
<div className={style.box2}></div>
{/* 当css中为短横线命名时,不能直接点,需要写成中括号的形式 */}
<div className={style['head-title']}>短横线</div>
{/* 但是可以在vite中配置,这样就可以使用小驼峰了 */}
<div className={style.headTitle}>小驼峰</div>
</>
)
}
在vite.config.js中配置局部样式可使用小驼峰写法
3.2 react中使用sass预处理器
安装:pnpm i sass ,然后使用方法与上面一致
3.3 classnames优化类名控制
四、添加事件操作
要点
- event为合成事件,与原生的有区别
- 所有事件委托到容器元素
- 传参处理:箭头函数(推荐)、高阶函数
function App() {
// 高阶函数穿参
// handleClick(1)相当于直接调用了,也就是把return后面的值写在onClick的{}中
const handleClick = (num) => {
return (e) => {
console.log(num, e)
}
}
// 箭头函数传参
const handleClick2 = (e,num) => {
console.log(e,num)
}
return (
<>
<button onClick={handleClick(1)}>高阶函数传参</button>
<button onClick={(e) => handleClick2(e, 1)}>箭头函数传参</button>
// 箭头函数传参写法:传参加箭头,不传参正常写
<button onClick={handleClick2}>不传参也可接收e</button>
</>
)
}
export default App
函数加小括号和不加小括号的区别
handleClick:表示一个函数表达式handleClick():表示函数执行完的结果,return后面的表达式
五、条件渲染
- 条件语句
if或switch - 三目运算符
- 逻辑运算符
&&和||
react的{}中,哪些值不会被渲染:布尔值、空字符串、null、undefined、对象、函数- 如何对不渲染的值进行输出:
JSON.stringify(),{undefined+''}
六、列表渲染
要点
- 通过 JavaScript 的
map()方法从数组中生成组件 - 通过 JavaScript 的
filter()筛选需要渲染的组件 - 何时以及为何使用 React 中的
key
七、组件相关
7.1 组件的点标记写法
- 对象形式
- 函数形式
// 组件的点标记写法:对象形式
const Qf = {
Welcome() {
return <div>小明</div>
},
}
// 解构赋值
const { Welcome } = Qf
function App() {
return (
<div>
<Qf.Welcome></Qf.Welcome>
<Welcome></Welcome>
</div>
)
}
export default App
// 组件的点标记写法:函数形式
const Af = () => {
return <div>小强</div>
}
Af.Hello = () => {
return <div>你好小强</div>
}
//解构赋值
const { Hello } = Af
function App() {
return (
<div>
<Af></Af>
<Af.Hello></Af.Hello>
<Hello></Hello>
</div>
)
}
export default App
7.2 组件通信
- props传递值:可以通过整体接收和解构接收
- 通过{...}批量传输数据
- props传递事件/函数
// 传递值
const ChildItem = (props) => {
return (
<div>
<div>{props.name}</div>
<div>{props.age}</div>
</div>
)
}
function App() {
const userInfo = {
name: 'zs',
age: 18,
}
return (
<div>
<ChildItem name={userInfo.name} age={userInfo.age}></ChildItem>
// 通过{...}批量传输数据
<ChildItem {...userInfo}></ChildItem>
</div>
)
}
export default App
// 传递函数
const ChildItem = ({ onClick, getData }) => {
getData('我是子组件的数据')
return (
<div>
<button onClick={onClick}>点击</button>
</div>
)
}
function App() {
const handleClick = () => {
console.log(111)
}
const getData = (data) => {
console.log(data)
}
return (
<div>
// 传递函数
<ChildItem onClick={handleClick} getData={getData}></ChildItem>
</div>
)
}
export default App
7.3 组件组合
要点
props的children属性- 如何分别传递多组内容
- 在组件标签之间写东西时(类似于插槽),会向下传递一个
props,其中有children属性
function Father() {
const count = 123
return (
<div>
<div>我是father</div>
<Son>
<GrandSon count={count}></GrandSon>
</Son>
</div>
)
}
function Son({ children }) {
const count = 456
return (
<div>
<div>我是son</div>
{children}
</div>
)
}
function GrandSon({ count }) {
return (
<div>
<div>我是GrandSon</div>
<div>传值{count}</div>
</div>
)
}
export default Father
- 如何分别传递多组内容(利用一个特性:只要是大括号能接收的,都能进行传递)
function Father() {
return (
<div>
<div>我是father</div>
<Son top={<div>aaa</div>} bottom={<div>bbb</div>}></Son>
</div>
)
}
function Son({ top, bottom }) {
return (
<div>
{top}
<div>我是son</div>
{bottom}
</div>
)
}
export default Father
7.4 组件通信的默认值
两种方式
- es6的默认参数
- react提供的组件defaultProps属性
function Father() {
return (
<div>
<div>我是father</div>
<Son></Son>
</div>
)
}
// 通过defaultProps属性添加默认值
Son.defaultProps = {
name: 'zs',
}
function Son({ name }) {
return (
<div>
<div>我是son</div>
<div>{name}</div>
</div>
)
}
export default Father
7.5 限定组件通信的类型
有两种方法
- ts
- 组件的
propTypes属性
其中,组件的propTypes属性配合 prop-types插件 可以做更细致的校验
7.6 组件必须是纯函数
纯函数的特点
- 只负责自己的任务,不更改在函数调用前就已存在的对象或变量
- 输入相同,则输出相同。给定相同的输入,总是返回相同的结果
8. 状态管理
8.1 什么是组件的状态
- 随时间变化的数据称之为状态(state),状态可以进行数据驱动视图,而普通变量不行
- useState可创建状态和修改状态的方法
import { useState } from 'react'
function App() {
// 可以有记忆功能
const [count, setCount] = useState(0)
const addCount = () => {
// 可以重新触发函数组件的执行
setCount(count + 1)
}
// 每次重新执行函数的count值都是不一样的,所以return的内容不一样
console.log(count)
return (
<div>
<button onClick={addCount}>点击+1</button>
<div>{count}</div>
</div>
)
}
export default App
8.2 状态是如何改变视图的
- 普通函数为什么不行:无法重新渲染JSX
- state状态为什么可行:重新触发函数组件,并且state状态具备组件的记忆
状态是如何改变视图的(渲染与提交的过程:三个步骤)
- 触发一次渲染:组件的初次渲染,
createRoot().render(),内部状态更新,触发渲染送入队列 - 渲染您的组件:在进行初次渲染时,React会调用根组件内部状态更新,会渲染对应的函数组件
- 提交到DOM上:初次渲染,通过
appendChild()使得内部状态更新,更新差异的DOM节点
8.3 如何记忆多状态
- 在同一组件的每次渲染中,
useState都依托于一个稳定的调用顺序。
在React内部,为每个组件保存了一个数组,其中每一项都是一个state对,它维护当前state对的索引值,在渲染之前将其设置为0,每次调用useState时,React都会提供一个state对,并增加索引值
- 不要在逻辑中调用
useState,会改变内部的顺序
8.4 什么是状态的快照以及快照的陷阱
- 虽然
state变量看起来和一般的可读写的JavaScript变量类似,但state的特性更像是一张快照。设置它不会更改已有的state变量,但会触发重新渲染 React会使state的值始终固定在一次渲染的各个事件处理函数内部,你无需担心代码运行到state是否发生了变化,其实这就是闭包的特性
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
console.log(count) // 0
}
return (
<div>
<button onClick={handleClick}>点击我+1</button>
<div>{count}</div>
</div>
)
}
export default App
8.5 状态队列与自动批处理
-
React会等到事件处理函数中的所有代码运行完毕再处理你的state更新,队列都执行完毕后,再进行UI更新,这种特性就是自动批处理 -
更新函数的写法:
setState(x)实际上会像setState((n)=>x)一样运行,只是没有使用n
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
// setCount(count + 1) // 0+1
// setCount(count + 1) // 0+1
// setCount(count + 1) // 0+1
// setCount(count+1)相当于setCount((c)=>count+1)
console.log(count) // 0
setCount((c) => c + 1) // 0+1
setCount((c) => c + 1) // 1+1
setCount((c) => c + 1) // 2+1
}
return (
<div>
<button onClick={handleClick}>点击我+1</button>
<div>{count}</div>
</div>
)
}
export default App
8.6 状态不可变
默认情况下,修改状态的值跟上一次相同的情况下,不会触发重新渲染(注意内部可能会自检)
import { useState } from 'react'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
// 错误的做法,对于引用数据而言,并没有改变,所以不会触发重新渲染
list.push({id:4,text:'ddd'})
setList(list)
// 正确的做法
setList([...list, { id: 4, text: 'ddd' }])
}
return (
<div>
<button onClick={handleClick}>点击我添加一项</button>
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
8.7 常见的对象和数组的解决方案
数组
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [list, setList] = useState([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
// 添加到最后一项
setList([...list, { id: 4, text: 'ddd' }])
// 添加到第二项
setList([...list.slice(0, 1), { id: 4, text: 'ddd' }, ...list.slice(1)])
// 删除id为3的项
setList(list.filter((item) => item.id !== 3))
// 将bbb替换为ddd
setList(
list.map((item) => {
if (item.id === 2) {
return { ...item, text: 'ddd' }
} else {
return item
}
})
)
// }
// 倒序,需要先将数组复制一份
// 拓展运算符,浅拷贝,只适用于一层的引用数据类型
const cloneList = [...list]
cloneList.reverse()
setList(cloneList)
// lodash库实现深拷贝
const cloneList = cloneDeep(list)
cloneList.reverse()
setList(cloneList)
}
return (
<div>
<button onClick={handleClick}>点击按钮</button>
<ul>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
对象
- 整体修改
- 利用拓展运算符,可以不用修改整体,但是如果想更新一个嵌套的属性,得多次使用(可以使用
lodash库的深拷贝,但是比较耗费性能)
import { useState } from 'react'
import { cloneDeep } from 'lodash'
function App() {
const [info, setInfo] = useState({
name: {
first: 'zhang',
last: 'san',
},
age: 19,
gender: '男',
})
const handleClick = () => {
// 1.多个拓展运算符
setInfo({
...info,
name: {
...info.name,
first: 'li',
},
})
// 2.lodash库
const cloneInfo = cloneDeep(info)
cloneInfo.name.first = 'li'
setInfo(cloneInfo)
}
return (
<div>
<button onClick={handleClick}>点击按钮</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
8.8 immer插件简化不可变数据结构
安装:pnpm i immer use-immer
- 如上面所示,对于多层嵌套的对象,处理起来很麻烦,且lodash库的深拷贝性能差,因此可以用 immer 来简化操作
immer是一个第三方模块,可以让你以更方便的方式处理不可变状态(相对于深拷贝,它使拷贝相对便宜,不需要复制数据树的未更改部分,并且在内存中与相同状态的旧版本共享)
操作对象
import { useImmer } from 'use-immer'
function App() {
const [info, setInfo] = useImmer({
name: {
first: 'zhang',
last: 'san',
},
age: 19,
gender: '男',
})
const handleClick = () => {
// draft相当于当前状态的一个副本
setInfo((draft) => {
draft.name.first = 'li'
})
}
return (
<div>
<button onClick={handleClick}>点击按钮</button>
<div>{JSON.stringify(info)}</div>
</div>
)
}
export default App
操作数组
import { useImmer } from 'use-immer'
function App() {
const [info, setInfo] = useImmer([
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
])
const handleClick = () => {
// draft相当于当前状态的一个副本,这样就可以使用数组的各种方法了
setInfo((draft) => {
draft.pop()
draft.push({ id: 4, text: 'ddd' })
draft.splice(1, 1, { id: 4, text: 'ddd' })
})
}
return (
<div>
<button onClick={handleClick}>点击按钮</button>
<ul>
{info.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
export default App
8.9 惰性初始化值
当状态的初始值需要经过复杂运算得到时,可以对其进行惰性初始化操作
import { useState } from 'react'
function computed(n) {
console.log(123)
return n + 1 + 2 + 3
}
function App() {
const [count, setCount] = useState(computed(3)) // 每次都会打印123
const [count, setCount] = useState(() => computed(3)) // 惰性初始化值
const handleClick = () => {
return setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击+1</button>
<div>{count}</div>
</div>
)
}
export default App
8.10 状态提升来解决共享问题
多次渲染同一个组件,每个组件都会拥有自己的state。状态独立且不共享
- 状态独立
import { useState } from 'react'
function Button() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>按钮</button>
// 行内样式小驼峰,需要vite配置后才能用
<span style={{ marginLeft: '10px' }}>{count}</span>
</div>
)
}
function App() {
return (
<div>
// 此时两个组件的状态state是独立的
<Button></Button>
<Button></Button>
</div>
)
}
export default App
- 状态提升
import { useState } from 'react'
function Button({ count, onClick }) {
return (
<div>
<button onClick={onClick}>按钮</button>
<span style={{ marginLeft: '10px' }}>{count}</span>
</div>
)
}
function App() {
// 状态提升到App组件中,实现状态共享
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<Button count={count} onClick={handleClick}></Button>
<Button count={count} onClick={handleClick}></Button>
</div>
)
}
export default App
8.11 状态的重置问题
- 当组件被销毁时,所对应的状态也会被重置
- 当组件位置没有发生改变时,状态会被保留
- 重置状态:1.不同的结构体 2.给组件添加key属性
import { useState } from 'react'
function Count() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击+1</button>
<div>{count}</div>
</div>
)
}
function App() {
const [isShow, setIsShow] = useState(true)
// 切换显示与隐藏
const changeShow = () => {
setIsShow(!isShow)
}
return (
<div>
<button onClick={changeShow}>切换显示隐藏</button>
{isShow && <Count></Count>}
</div>
)
}
export default App
import { useState } from 'react'
function Counter({ style }) {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button style={style} onClick={handleClick}>
计数
</button>
<div>{count}</div>
</div>
)
}
function App() {
const [isStyle, setIsStyle] = useState(false)
const handleClick = () => {
setIsStyle(true)
}
return (
<div>
<button onClick={handleClick}>添加样式</button>
{isStyle ? (
<Counter style={{ border: '1px solid red' }}></Counter>
) : (
<Counter></Counter> // 结构相同,状态保留
<div> <Counter></Counter> </div> // 结构不同,会重置状态
<Counter key='con2'></Counter> // 给组件加key,会重置状态
)}
</div>
)
}
export default App
8.12 状态的计算变量
由于状态会重新渲染函数组件,可以利用当前状态快照生成对应的计算变量
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
// 计算变量,类似于vue中的计算属性
const count2 = count * 2
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>{count}</div>
<div>{count2}</div>
</div>
)
}
export default App
8.13 受控组件与非受控组件
- 受控组件:通过
props控制的组件 - 非受控组件:通过
state控制的组件
注意:React表单内置了受控组件的行为
value+onChange(输入框和下拉框)checked+onChange(单选框和多选框)
import { useState } from 'react'
function App() {
// 输入框
const [value, setValue] = useState('')
const handleChange = (e) => {
setValue(e.target.value)
}
// 多选框
const [checked, setChecked] = useState(false)
const changeChecked = (e) => {
setChecked(e.target.checked)
}
return (
<div>
{/* 输入框 */}
<input type='text' value={value} onChange={handleChange} />
<div>{value}</div>
{/* 多选框 */}
<input type='checkbox' checked={checked} onChange={changeChecked} />
<div>{checked + ''}</div>
</div>
)
}
export default App
8.14 实战案例 todolist
import { useState } from 'react'
import { useImmer } from 'use-immer'
function TaskList({ title, task, handleCheck }) {
return (
<div>
<div>{title}</div>
<ul>
{task.map((item) => (
<li key={item.id}>
<input
type='checkbox'
checked={item.isOver}
onChange={(e) => handleCheck(e, item.id)}
/>
<span
style={item.isOver ? { textDecoration: 'line-through' } : null}>
{item.task}
</span>
</li>
))}
</ul>
</div>
)
}
function Todo() {
// 输入框
const [value, setValue] = useState('')
const onChange = (e) => {
setValue(e.target.value)
}
// 任务列表
const [task, setTask] = useImmer([])
// 添加任务
const onClick = () => {
setTask((draft) => {
draft.push({ id: task.length, task: value, isOver: false })
})
setValue('')
}
// 未完成的任务数
const noOver = task.filter((item) => item.isOver === false)
// 已完成的任务数
const yesOver = task.filter((item) => item.isOver === true)
// 点击多选框
const handleCheck = (e, id) => {
setTask((draft) => {
draft.find((item) => item.id === id).isOver = e.target.checked
})
}
return (
<div>
<input type='text' value={value} onChange={onChange} />
<button onClick={onClick}>添加任务</button>
<TaskList
title={<h2>未完成的任务:{noOver.length}个</h2>}
task={task.filter((item) => item.isOver === false)}
handleCheck={handleCheck}></TaskList>
<TaskList
title={<h2>已完成的任务:{yesOver.length}个</h2>}
task={task.filter((item) => item.isOver === true)}
handleCheck={handleCheck}></TaskList>
</div>
)
}
export default Todo