笔记指向课程:b站吴悠讲编程,如有错误欢迎指正~
1. 创建项目
- 终端
npx create-next-app 项目名 - cd 项目名 进入项目目录
npm start启动项目
1.1 一些介绍
两个重要文件
-
index.js为入口文件,引入了两个库React、ReactDOM,通过ReactDOM.creatRoot创建一个react实例,通过render渲染根组件,React.StrictMode进行组件内部的功能审查 -
React组件的两种创建方式:函数组件(主推)、类组件
2. JSX
即js和html语法写一起
2.1 基本用法
1) 函数组件return后面的小括号(),不换行可不写小括号
2)jsx只能返回一个根元素,如果必须设计多级,两方法
-
再写个容器包住
-
用空标签包住(渲染时不会产生多余标签)
return( <> <div>1</div> <div>2</div> </> )
2.2 数据插值
2.2.1基础使用
插值可以使用的位置: 1.标签位置 2.标签内容
function App() {
const divContent = '标签内容'
const divTitle = '标签标题'
return (
<div title={divTitle}>{divContent}</div>
)
}
但是如果你写成<div title="{divTitle}">{divContent}</div> 给插值加上了括号,title就变成普通属性了
2.2.2条件渲染
function App() {
const divTitle = '标签标题'
let divContent = null
const flag = false
if(flag){
divContent = <span>flag为true</span>
}else{
divContent = <p>flag为false</p>
}
return (
<div title={divTitle}>{divContent}</div>
)
}
注意不要加引号写成divContent = '<span>flag为true</span>'
2.2.3列表渲染
import { Fragment } from 'react'
function App() {
const list = [
{id: 1,name:'小吴'},
{id: 2,name:'小李'},
{id: 3,name:'小花'},
]
const listContent = list.map(item=>(
<Fragment key={item.id}>
<li>{item.name}</li>
<li>---------</li>
</Fragment>
))
return (
<ul>{listContent}</ul>
)
}
由于只能有一个根目录,所以map下两个li元素可以写成如下形式
const listContent = list.map(item=>(
<>
<li key={item.id}>{item.name}</li>
<li>---------</li>
</>
))
如果<li>---------</li>也需要遍历展示呢?
我们知道 空标签<></>是不会被渲染成结构的,因此key定义在空标签上是不会生效的
这时可使用 React提供的Fragment标签,同时要记得在前面import引用该标签
2.2.4事件处理
jsx属性大多使用驼峰命名
function App() {
function handleClick(e){
console.log('点击按钮',e)
}
return (
<button onClick={handleClick}>按钮</button>
)
}
2.2.5状态处理
字符串的状态更改
import { useState } from 'react'
function App() {
const [content,setContent] = useState('标签的默认内容')
function handleClick(e){
setContent('新内容')
}
return (
<>
<div>{content}</div>
<button onClick={handleClick}>按钮</button>
</>
)
}
useState是react提供的一个函数,解构出来的content代表本次渲染的内容,setContent是一个用来修改这个状态内容的函数,能实现类似于vue响应式的效果
对象的状态更改
import { useState } from 'react'
function App() {
const [data,setData] = useState({
title:'默认标题',
content:'默认内容'
})
function handleClick(){
setData({
...data,
title: '新标题'
})
}
return (
<>
<div title={data.title}>{data.content}</div>
<button onClick={handleClick}>按钮</button>
</>
)
}
对于对象的状态操作,其实和字符串差不多,唯一一个要注意的点是,在setData修改内容的时候,不能只写修改内容的操作,即
function handleClick(){
setData({
title: '新标题'
})
}
这样会造成其他属性内容的丢失,在修改属性操作前加上...data拓展操作,即把其他不变属性也添加进来,便不会造成内容丢失
数组的状态更改
function App() {
const [data,setData] = useState([
{id: 1,name:'小吴'},
{id: 2,name:'小李'},
{id: 3,name:'小花'},
])
const listData = data.map(item=>(
<li key={item.id}>{item.name}</li>
))
function handleClick(){
setData(data.filter(item=>item.id!==2))
}
return (
<>
<ul>{listData}</ul>
<button onClick={handleClick}>按钮</button>
</>
)
}
3.组件通信和插槽
3.1一些基础语法
1)看起来像html属性的功能,在react中称为为DOM组件设置Props
举例1
import image from 'XXX'
<img
src={image}
alt=""
/>
对于src第一种方法是 import(如示例),第二种方法是使用在线地址
举例2
vue中的class属性 对应 react中的className
className=''
举例3
style属性采用键值对写法
style={{
width:100, // width: '100px'
height:100
}}
如果要加单位,要添加引号
但是更推荐下面的写法
const imgStyle={
xxx样式内容
}
style={imgStyle}
举例4
在react使用css中带-的属性,例如background-color,一般要写成驼峰形式,如backgroundColor
3.2jsx展开语法和props
import image from './logo.svg';
function App() {
const imgData = {
className:'small',
style:{
width: 100,
height: 100,
backgroundColor: 'grey'
},
src:image
}
return (
<div>
<img
alt=""
{...imgData}
/>
</div>
)
}
在一个标签里设置太多属性,书写繁琐
react便提供了一种展开语法,将属性集中写在一个对象中,再应用在标签内(其中{...imgData}中的大括号是jsx单独的功能支持)
注:jsx的展开操作不是es6中的展开运算符
jsx的展开操作必须写在一个容器中,例如<img/>,但是es6的展开运算符是可以单独暴露出来的,如const a = {...b}
但是如果是要打印console.log()imgData中的内容,不能写成console.log(...imgData),要写成console.log({...imgData})
3.3react自定义组件
function Article(props) {
return (
<div>
<h2>{props.title}</h2>
<p>{props.content}</p>
<p>状态{active?'显示中':'已隐藏'}</p>
</div>
)
}
export default function App(){
return(
<>
<Article
title='标题1'
content='内容1'
/>
<Article
title='标题2'
content='内容2'
/>
<Article
title='标题3'
content='内容3'
/>
</>
)
};
也可以不写成props点xxx的形式,需要使用到es6的解构
function Article({title,content,active}) {
return (
<div>
<h2>{title}</h2>
<p>{content}</p>
<p>状态{active?'显示中':'已隐藏'}</p>
</div>
)
}
3.3.1react组件使用props
父传子
传递的值不可修改
function Detail({content,active}){
return(
<>
<p>{content}</p>
<p>状态{active?'显示中':'已隐藏'}</p>
</>
)
}
function Article({title,articleData}) {
return (
<div>
<h2>{title}</h2>
<Detail {...articleData}/>
</div>
)
}
export default function App(){
const articleData = {
title:'标题1',
detailData:{
content:'内容1',
active:true
}
}
return(
<>
<Article
{...articleData}
/>
</>
)
};
3.3.2react插槽功能
function List({children}){
return(
<ul>
{children}
</ul>
)
}
export default function App(){
return(
<>
<List>
<li>列表项1</li>
<li>列表项1</li>
<li>列表项1</li>
</List>
<List>
<li>列表项2</li>
<li>列表项2</li>
<li>列表项2</li>
</List>
</>
)
};
3.3.3react多处插槽
注意,对于可选值,例如下面代码中的footer,有可能有 有可能没有,这种情况要设置默认值
function List({children,title,footer=<div>默认底部</div>}){
return(
<>
<h2>{title}</h2>
<ul>
{children}
</ul>
{footer}
</>
)
}
export default function App(){
return(
<>
<List
title='标题1'
footer={<p>这是底部内容1</p>}
>
<li>列表项1</li>
<li>列表项1</li>
<li>列表项1</li>
</List>
<List
title='标题2'
footer={<p>这是底部内容1</p>}
>
<li>列表项A</li>
<li>列表项B</li>
<li>列表项C</li>
</List>
<List
title='标题3'
>
<li>列表项X</li>
<li>列表项Y</li>
<li>列表项Z</li>
</List>
</>
)
};
3.3.4子传父
import {useState} from 'react'
function Detail({onActive}){
const [status,setStatus] = useState(false)
function handleClick(){
setStatus(!status)
onActive(status)
}
return(
<div>
<button onClick={handleClick}>按钮</button>
<p style={{
display:status?'block':'none'
}}>Detail的内容</p>
</div>
)
}
export default function App(){
function handleActive(status){
console.log(status)
}
return(
<>
<Detail
onActive={handleActive}
/>
</>
)
};
3.3.5同级组件传值
import {useContext, createContext, useState} from 'react'
export function Section({children}){
const level = useContext(LevelContext)
return(
<section>
<LevelContext.Provider value={level+1}>
{children}
</LevelContext.Provider>
</section>
)
}
export function Heading({children}){
const level = useContext(LevelContext)
switch(level){
case 1:
return <h1>{children}</h1>
case 2:
return <h2>{children}</h2>
case 3:
return <h3>{children}</h3>
case 4:
return <h4>{children}</h4>
case 5:
return <h5>{children}</h5>
case 6:
return <h6>{children}</h6>
default:
throw Error('未知的level:'+level)
}
}
const LevelContext = createContext(1)
export default function App(){
return(
<div>
<Section>
<Heading>主标题</Heading>
<Section>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Heading>副标题</Heading>
<Section>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Heading>子标题</Heading>
<Section>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
<Heading>子子标题</Heading>
</Section>
</Section>
</Section>
</Section>
</div>
)
}
LevelContext.Provider 提供一个上下文值,React 的 Context 机制 是 从外层向内层(自上而下)传递数据的。
因此上面的代码块运行过程如下:
- 最外层的
Section没有上级 Provider,所以使用默认值 1 - 它内部的
Heading就会渲染为 h1 - 嵌套在里面的
Section会把级别增加到 2,所以它的Heading会渲染为 h2 - 再往内一层就是 h3,以此类推
<LevelContext.Provider value={level + 1}>会动态更新level,并且 内层的Heading组件会实时获取最新的level值,因为useContext(LevelContext)会自动订阅最近的Provider的变化
4. React Hooks
4.1useReducer
用于管理比 useState 更复杂的状态逻辑,特别适合状态逻辑较复杂或包含多个子值的场景。
基本语法:
const [state, dispatch] = useReducer(reducer, initialState);
- reducer:一个函数,形式为
(state, action) => newState - initialState:状态的初始值
import {useReducer} from 'react'
function countReducer (state,action){
switch(action.type){
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
throw new Error()
}
}
export default function App(){
// 计算器
const [count,dispatch] = useReducer(countReducer,0)
const handleIncrement = () => dispatch({type:'increment'})
const handleDecrement = () => dispatch({type:'decrement'})
return(
<div style={{padding:10}}>
<button onClick={handleDecrement}>-</button>
<span>{count}</span>
<button onClick={handleIncrement}>+</button>
</div>
)
}
- 对于
const [count,dispatch] = useReducer(countReducer,0):
- 初始化状态:
count初始值为0(第二个参数)。 - 定义状态更新规则:
countReducer是一个函数,决定如何根据action更新count。 - 返回当前状态和派发函数:
count:当前的状态值(这里是数字)。dispatch:用于触发状态更新的函数(发送action给countReducer)。
- 对于
const handleIncrement = () => dispatch({type:'increment'}):
type属于一个只是一个普通的对象属性名,可以自由命名,例如写成handleDecrement → dispatch({ abc: 'decrement' })判断条件则写成action.abc = 'decrement'
4.2useRef
useRef 返回一个可变的 ref 对象({ current: undefined }),其 .current 属性可以存储任意值。ref 的变动不会触发组件重新渲染(与 state 不同)
import {useRef,useState} from 'react'
export default function App() {
const [count, setCount] = useState(0)
const prevCount = useRef()
function handleClick() {
prevCount.current = count // 在更新前,保存当前的 count 到 prevCount
setCount(prevCount.current + 1) // 更新 count
}
return(
<div>
<p>最新的count:{count}</p>
<p>上一次的count:{prevCount.current}</p>
<button onClick={handleClick}>增大count</button>
</div>
)
}
4.3useImperativeHandle & forwardRef
import {forwardRef, useRef, useImperativeHandle} from 'react'
const Child = forwardRef(function (props, ref)
// Child 组件被 forwardRef 包装,可以接收 ref
useImperativeHandle(ref, () => ({
// 子组件暴露给父组件的方法
handleClick: () => {
console.log('子组件方法')
}
}))
return <div>子组件</div>
})
export default function App() {
const childRef = useRef()
function handleClick() {
childRef.current.handleClick()
}
return(
<div>
<Child ref={childRef}/>
<button onClick={handleClick}>点击</button>
</div>
)
}
4.4forwardRef
forwardRef 允许 子组件接收父组件传递的 ref,并将其绑定到子组件内部的 DOM 或方法上。
为什么需要 forwardRef?
- 默认情况下,函数组件不能直接接收
ref(因为函数组件没有实例)。 forwardRef包装后,子组件可以接收ref并决定如何暴露内部内容。props:父组件传递的属性。ref:父组件传递的ref对象。
4.5useImperativeHandle
useImperativeHandle 允许 子组件自定义暴露给父组件的内容(如方法、属性),而不是直接暴露 DOM 节点。
为什么需要 useImperativeHandle?
-
默认情况下,
ref只能访问 DOM 节点(如<div ref={ref}>)。 -
使用
useImperativeHandle,可以暴露 子组件的特定方法或数据,而不是整个 DOM。 -
ref:父组件传递的ref。 -
回调函数:返回一个对象,定义暴露的内容
4.6useEffect
用于在函数组件中执行副作用操作
执行时机:
-
没有依赖数组:每次渲染后都会执行
useEffect(() => { console.log('每次渲染后执行'); }); -
空依赖数组:仅在组件挂载时执行一次
useEffect(() => { console.log('仅在组件挂载时执行'); }, []); -
有依赖项:依赖项变化时执行
useEffect(() => { console.log('count变化时执行'); }, [count]);
4.7useMemo
用于缓存计算结果,避免在每次渲染时都进行不必要的复杂计算
基本语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 第一个参数:一个函数,返回需要缓存的值
- 第二个参数:依赖项数组,只有当依赖项发生变化时才会重新计算
demo演示
import { useState } from "react";
function DoSomeMath({ value }){
console.log('DoSomeMath执行了')
let result = 0
for(let i=0;i<1000000;i++){
result += value * 2;
}
return(
<div>
<p>输入内容:{value}</p>
<p>经过复杂计算的数据:{result}</p>
</div>
)
}
function App() {
const [inputValue, setInputValue] = useState(5);
const [count, setCount] = useState(0);
return(
<div>
<p>count的值为:{count}</p>
<button
onClick={() => setCount(count + 1)}
>点击更新</button>
<br />
<br />
<input
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<DoSomeMath value={inputValue} />
</div>
)
}
export default App;
在这个demo中,DoSomeMath相当于一个复杂计算,针对value进行计算。我们在DoSomeMath添加调试信息console.log('DoSomeMath执行了')会发现改变count值时,也会执行DoSomeMath复杂计算。
这时就需要使用到useMemo
function DoSomeMath({ value }){
const result = useMemo(() => {
console.log('DoSomeMath执行了')
let result = 0
for(let i=0;i<1000000;i++){
result += value * 2;
}
return result
},[value])
return(
<div>
<p>输入内容:{value}</p>
<p>经过复杂计算的数据:{result}</p>
</div>
)
}
要注意,需要添加[value]这个依赖项数组,表示当value发生变化后才会执行复杂计算
4.8useCallBack
用于缓存函数引用,在依赖项不变的情况下返回相同的函数实例,避免子组件不必要的重新渲染。
基本语法:
const memoizedCallback = useCallback(
() => {
// 函数逻辑
doSomething(a, b);
},
[a, b], // 依赖项数组
);
demo演示
import { useCallback, useState, memo } from "react";
const Button=memo(function ({onClick}) {
console.log('Button渲染了');
return (
<button onClick={onClick}>
子组件
</button>
)
})
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('点击了');
}
const handUpdate = () => {
setCount(count + 1);
}
return(
<div>
<p>Count:{count}</p>
<button onClick={handUpdate}>点击</button>
<br />
<Button onClick={handleClick} />
</div>
)
}
export default App
上面的demo例子,如果父组件App重新渲染会导致子组件也被迫重新渲染,即点击<button onClick={handUpdate}>点击</button>会同时执行handleClick
memo的作用是:memo 会记忆(memoize)组件,只有当它的 props 发生变化时才会重新渲染
想要避免父组件重新渲染时导致不必要的子组件重新渲染,还需要使用useCallBack函数
const handleClick = useCallback(() => {
console.log('点击了');
},[])
demo的工作原理:
- 当点击"点击"按钮时:
count状态更新App组件重新渲染- 如果没有
useCallback,每次渲染都会创建一个新的handleClick函数实例 - 新的函数实例会导致
memo认为Button的 props 发生了变化,从而重新渲染Button
- 使用
useCallback后:handleClick保持不变memo检测到Button的 props 没有变化Button不会重新渲染(控制台不会打印"Button渲染了")