React简单介绍
一句话:想进大厂都得会React
React开发环境搭建-create-react-app
create-react-app是一个创建React开发环境的工具,底层由WebPack构建,封装了配置细节
创建命令:npx create-react-app 你的项目名称
启动:npm start
目录结构讲解
介绍一下目录结构,整体结构如图所示:
index.js(react的入口文件)
//React必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
JSX
概念和本质
什么是JSX:
JSX是JS和XML(HTML)的缩写,表示在JS代码中编写HTML模板结构,它是React中编写UI模板的方式
JSX的本质:JSX并不是标准的JS语法,它是JS的语法扩展,浏览器本身不能识别,需要通过Babel解析工具做解析之后才能在浏览器中运行。
JSX使用JS表达式
在JSX中可以通过大括号语法{}识别JS表达式,比如常见的变量、函数调用、方法调用等。
let name = "张三"
function getAge() {
return 23
}
// 项目的根组件
function App() {
return (
<div className="App">
this is App
{/* 使用引号传递字符串 */}
{'hello world'}
{/* 使用js变量 */}
姓名:{name}
{/* 函数调用和方法调用 */}
年龄:{getAge()}
{/* 使用js对象 */}
<span style={{ color: 'red' }}>足球</span>
</div>
);
}
export default App;
注意:if、switch、变量声明属于语句,不能出现在{}中
JSX实现列表渲染
const list = [
{
id: 1, name: '数学'
},
{
id: 1, name: '语文'
},
{
id: 1, name: '英语'
},
{
id: 1, name: '物理'
}
]
// 项目的根组件
function App() {
return (
<div className="App">
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
条件渲染
略,本质也是使用js条件判断语句
React中的事件绑定
语法:on + 事件名称={事件处理程序}
const handleClickBtn = () => {
console.log('点击按钮');
}
// 项目的根组件
function App() {
return (
<div className="App">
<button onClick={handleClickBtn}>按钮</button>
</div>
);
}
export default App;
React组件基础使用
在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI,渲染组件只需要把组件当成标签书写即可
useState基本使用
useState是一个React Hook,它允许我们向组件添加一个状态变量。
本质:和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
const [count,setCount]=useState(0)
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量,第二个参数是set函数用来修改状态变量
- useState的参数将作为count的初始值
案例:
import { useState } from "react";
// 项目的根组件
function App() {
let [count, setCount] = useState(0)
const handleMyButton = () => {
setCount(count + 1)
}
return (
<div className="App">
<button onClick={handleMyButton}>点击增加{count}</button>
</div>
);
}
export default App;
useState修改状态的规则
状态不可变:在React,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新
修改对象状态:对于对象类型的状态变量,应该始终传给set方法一个全新的对象来进行修改
组件的样式处理
import { useState } from "react";
import "./App.css";
// 项目的根组件
function App() {
let [count, setCount] = useState(0)
let style = {
color: 'blue'
}
const handleMyButton = () => {
setCount(count + 1)
}
return (
<div className="App">
{/* 行内样式,不推荐 */}
<span style={style}>测试一下</span>
{/* 通过类名设置样式 */}
<button onClick={handleMyButton} className="btn-text">点击增加{count}</button>
</div>
);
}
export default App;
classNames类名控制工具
简介:可以帮助我们更好、更直观地管理样式类名
安装后即可使用,案例:
import { useState } from "react";
import "./App.css";
import classNames from 'classnames';
// 项目的根组件
function App() {
let [count, setCount] = useState(0)
let style = {
color: 'blue'
}
const handleMyButton = () => {
setCount(count + 1)
}
return (
<div className="App">
{/* 行内样式,不推荐 */}
<span style={style}>测试一下</span>
{/* 通过类名设置样式 */}
<button onClick={handleMyButton} className={classNames({ 'btn': true, 'odd': count % 2 === 0, 'even': count % 2 !== 0 })}>点击增加{count}</button>
</div>
);
}
export default App;
受控表单绑定
思路:
- state绑定到input的value属性
- 把input最新的value值设置给state
import { useState } from "react";
import "./App.css";
// import classNames from 'classnames';
// 项目的根组件
function App() {
let [inputValue, setInputValue] = useState('');
const inputChange = (e) => {
setInputValue(e.target.value);
}
return (
<div className="App">
<input type="text" value={inputValue} onChange={inputChange} />
</div>
);
}
export default App;
React中获取Dom
在React中获取DOM需要使用useRef钩子函数
import { useState, useRef } from "react";
import "./App.css";
// import classNames from 'classnames';
// 项目的根组件
function App() {
let [inputValue, setInputValue] = useState('');
const inputChange = (e) => {
setInputValue(e.target.value);
console.log('input的dom', inputRef.current);
}
const inputRef = useRef(null);
return (
<div className="App">
<input ref={inputRef} type="text" value={inputValue} onChange={inputChange} />
</div>
);
}
export default App;
组件通信
父传子-基础实现
- 父组件传递数据-在子组件标签上
绑定属性 - 子组件接收数据-组件通过
props参数接收数据
import { useState, useRef } from "react";
import "./App.css";
function Son(props) {
// props:对象里面包含了父组件传递过来的所有数据
console.log('props', props);
return (
<div>{props.name}</div>
)
}
function App() {
let name = "子组件名称"
return (
<div className="App">
<Son name={name}></Son>
</div>
);
}
export default App;
父传子-props说明
- props可以传递任意的数据(包括jsx)
- props是只读对象,子组件
只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改
父传子-特殊的prop:children
场景:当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容3
import { useState, useRef } from "react";
import "./App.css";
function Son(props) {
// props:对象里面包含了父组件传递过来的所有数据
console.log('props', props);
return (
<div>{props.name}{props.children}</div>
)
}
function App() {
let name = "子组件名称"
return (
<div className="App">
<Son name={name}>
<span>this is a span</span>
</Son>
</div>
);
}
export default App;
子传父
核心思路:在子组件中调用父组件中的函数并传递参数
import { useState, useRef } from "react";
import "./App.css";
function Son({ getSonMsg }) {
const handleButton = () => {
getSonMsg('this is sonMsg')
}
return (
<button onClick={handleButton}>子组件按钮</button>
)
}
function App() {
const getSonMsg = (msg) => {
console.log('msg', msg);
}
return (
<div className="App">
<Son getSonMsg={getSonMsg}></Son>
</div>
);
}
export default App;
使用状态提升实现兄弟组件通信
状态提升机制其实就是通过父组件进行兄弟组件之间的数据传递:A组件通过子传父的方式把数据传递给父组件,父组件再使用父传子的方式传递给B组件
案例:
import "./App.css";
import { useState } from "react";
// A组件
function A({ getAName }) {
const handleClickBtn = () => {
getAName('this is A name')
}
return (
<button onClick={handleClickBtn}>send</button>
)
}
// B组件
function B(props) {
console.log('props---', props);
return (
<div>
A组件传递的数据:{props.aName}
</div>
)
}
function App() {
let [aName, setAName] = useState('')
const getAName = (data) => {
console.log('A组件传递的数据', data);
setAName(data)
}
return (
<div className="App">
<A getAName={getAName}></A>
<B aName={aName}></B>
</div>
);
}
export default App;
使用context机制跨层级传递数据(兄弟组件、跨层级组件都可以传值)
实现步骤:
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件中通过Ctx.Provider组件提供数据
- 在底层组件中通过useContext钩子函数获取数据
useEffect
概念理解与基础使用
useEffect是一个Hook函数,用于在React组件创建不是由事件引起而是由渲染本身引起的操作,比如发送AJAX请求,更改DOM等。
语法:useEffect(()=>{},[])
- 参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
- 参数2是一个数组(可选),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,
当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次
案例:在组件渲染完毕之后,立即从服务端获取频道列表数据并显示到页面中
import { useEffect, useState } from "react";
const URL = 'http://geek.itheima.net/v1_0/channels'
function App() {
const [list, setList] = useState([])
useEffect(() => {
// 额外的操作,获取频道列表
async function getList() {
const res = await fetch(URL)
const jsonRes = await res.json()
setList(jsonRes.data.channels)
}
getList()
}, [])
return (
<div className="App">
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
useEffect依赖项参数说明
useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现
| 依赖项 | 副作用函数执行时机 |
|---|---|
| 没有依赖项 | 组件初始渲染+组件更新时执行 |
| 空数组依赖 | 只在初始渲染时执行一次 |
| 添加特定依赖项 | 组件初始渲染+特性依赖项变化时执行 |
清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用。
useEffect(() => {
// 实现副作用操作逻辑
return () => {
// 清除副作用逻辑
}
}, [])
说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行
需求:在Son组件渲染时开启一个定时器,卸载时清除这个定时器
import { useEffect, useState } from "react";
function Son() {
// 1.渲染时开启一个定时器
useEffect(() => {
setInterval(() => {
console.log('定时器执行中----');
}, 1000)
}, [])
return <div>this is son</div>
}
function App() {
const [show, setShow] = useState(true)
return (
<div className="App">
{show && <Son />}
<button onClick={() => setShow(false)}>卸载Son组件</button>
</div>
);
}
export default App;
像上面这种情况哪怕卸载了Son组件,定时器依然存在,这时候就需要清除副作用
import { useEffect, useState } from "react";
function Son() {
// 1.渲染时开启一个定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行中----');
}, 1000)
return () => {
// 清除副作用(组件卸载时)
clearInterval(timer)
}
}, [])
return <div>this is son</div>
}
function App() {
const [show, setShow] = useState(true)
return (
<div className="App">
{show && <Son />}
<button onClick={() => setShow(false)}>卸载Son组件</button>
</div>
);
}
export default App;
自定义hook函数
概念:自定义hook是以use打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用
封装自定义hokk通用思路
- 声明一个use打头的函数
- 在函数体内封装可复用的逻辑(只要是可复用的逻辑)
- 把组件中用到的状态或者回调return出去(以对象或者数组)
- 在哪个组件中要用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用
案例:
import { useState } from "react";
function useToggle() {
const [state, setState] = useState(false)
const handleToggle = () => {
setState(!state)
}
return {
state,
handleToggle
}
}
function App() {
const { state, handleToggle } = useToggle()
return (
<div className="App">
{state && <div>this is a div</div>}
<button onClick={handleToggle}>toggle</button>
</div>
);
}
export default App;
React Hooks使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
Redux
什么是Redux?
Redux是React最常用的集中状态管理工具,类似于Vue的Vuex和pinia,可以独立于框架运行
环境准备
在React中使用redux,官方要求安装两个插件-Redux Toolkit 和 react-redux
- Redux Toolkit(RTX)-官方推荐编写Redux逻辑的方式,是一套工具的集合集,
简化书写方式
优点如下:
- 简化store的配置方式
- 内置immer支持可变式状态修改
- 内置thunk更好地异步创建
- react-redux-用来
连接Redux和React组件的中间件
配置基础环境:npm i @reduxjs/toolkit react-redux
store目录结构设计
- 通常集中状态管理的部分都会单独创建一个单独的
store目录 - 应用中有很多个store子模块,所以创建一个
modules目录,在内部编写业务分类的子store - store中的入口文件
index.js的作用是组合modules中所有子模块,并导出store
案例-实现counter
思维导图:
在Redux中修改数据的唯一方式是提交一个action
首先需要创建store
counterStore.js
import { createSlice } from '@reduxjs/toolkit'
const counterStore = createSlice({
name: 'counter',
// 初始化state
initialState: {
count: 0
},
// 修改状态的方法 同步方法 支持直接修改
reducers: {
inscrement(state) {
state.count++
},
decrement(state) {
state.count--
}
}
})
// 解构出来actionCreater函数
const { inscrement, decrement } = counterStore.actions
// 获取reducer
const reducer = counterStore.reducer
// 以按需导出的方式导出actionCreater
export { inscrement, decrement }
// 以默认导出的方式导出reducer
export default reducer
store/index.js
import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import counterReducer from './modules/counterStore'
const store = configureStore({
reducer: {
counter: counterReducer
}
})
export default store
使用react-redux为React注入store:react-redux负责把React与Redux连接起来,内置Provider组件通过store参数把创建好的store实例注入到应用中,链接正式建立
//React必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 建立Redux与React的连接
import { Provider } from 'react-redux';
import store from './store'
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
React组件使用store中的数据:在React组件中使用store中的数据,需要用到一个钩子函数-useSelector,它的作用是把store中的数据映射到组件中
import { useSelector } from "react-redux";
function App() {
const { count } = useSelector(state => state.counter)
return (
<div className="App">
{count}
</div>
);
}
export default App;
React组件修改store中的数据:React组件中修改store中的数据需要借助另外一个hook函数-useDispatch,它的作用是生成提交action对象的dispatch函数:
import { useSelector, useDispatch } from "react-redux";
// 导入actionCreater
import { inscrement, decrement } from './store/modules/counterStore'
function App() {
const { count } = useSelector(state => state.counter)
const dispatch = useDispatch()
return (
<div className="App">
<button onClick={() => dispatch(decrement())}>-</button>
{count}
<button onClick={() => dispatch(inscrement())}>+</button>
</div>
);
}
export default App;
提交action传参
在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
异步状态操作
具体步骤:
- 创建store的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部return一个新函数,在新函数中封装异步请求获取数据,调用
同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交 - 组件中dispatch的写法保持不变
channelStore.js
import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
const channelStore = createSlice({
name: 'channel',
// 初始化state
initialState: {
channelList: []
},
// 修改状态的方法 同步方法 支持直接修改
reducers: {
setChannels(state, action) {
state.channelList = action.payload
}
}
})
// 异步请求部分
const { setChannels } = channelStore.actions
const fetchChannelList = () => {
console.log('fetchChannelList');
return async (dispatch) => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
console.log('res-----', res);
dispatch(setChannels(res.data.data.channels))
}
}
export { fetchChannelList }
const reducer = channelStore.reducer
export default reducer
store/index.js
import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import counterReducer from './modules/counterStore'
import channelReducer from './modules/channelStore'
const store = configureStore({
reducer: {
counter: counterReducer,
channel: channelReducer
}
})
export default store
App.js组件
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
// 导入actionCreater
import { fetchChannelList } from './store/modules/channelStore'
function App() {
const { channelList } = useSelector(state => state.channel)
const dispatch = useDispatch()
// 使用useEffect触发异步
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
return (
<div className="App">
<ul>
{channelList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
React Router
快速开始
简单的一个路由案例:
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
// 1.创建router实例对象并且配置路由对应关系
const router = createBrowserRouter([
{
path: '/',
element: <div>我是首页</div>
},
{
path: '/login',
element: <div>我是登陆页</div>
},
{
path: '/article',
element: <div>我是文章页</div>
}
])
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<RouterProvider router={router}></RouterProvider>
</Provider>
);
抽象路由模块
第一步:将页面抽离为一个单独的模块
第二步: 创建路由模块
import Login from '../page/Login'
import Article from '../page/Article'
import { createBrowserRouter } from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/',
element: <div>首页</div>
},
{
path: '/login',
element: <Login />
},
{
path: '/article',
element: <Article />
}
])
export default router
第三步:入口文件渲染RouterProvider
//React必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 建立Redux与React的连接
import { Provider } from 'react-redux';
import store from './store'
import { RouterProvider } from 'react-router-dom';
import router from '../src/router'
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<RouterProvider router={router}></RouterProvider>
</Provider>
);
路由导航跳转
声明式导航
import { Link } from 'react-router-dom'
const Login = () => {
return (
<div>
我是登陆页
<Link to="/article">跳转到文章页</Link>
</div>
)
}
export default Login
编程式导航
import { useNavigate } from "react-router-dom"
const Login = () => {
const navigate = useNavigate()
return (
<div>
我是登陆页
<button onClick={() => navigate('/article')}>跳转到文章页</button>
</div>
)
}
export default Login
导航路由传参
searchParams传参
参数传递
import { useNavigate } from "react-router-dom"
const Login = () => {
const navigate = useNavigate()
return (
<div>
我是登陆页
<button onClick={() => navigate('/article')}>跳转到文章页</button>
<button onClick={() => navigate('/article?id=100&name=jack')}>searchParams传参</button>
</div>
)
}
export default Login
接收参数
import { useSearchParams } from "react-router-dom"
const Article = () => {
const [params] = useSearchParams()
let id = params.get('id')
let name = params.get('name')
return <div>我是文章页 id:{id} name:{name}</div>
}
export default Article
params传参
import { useNavigate } from "react-router-dom"
const Login = () => {
const navigate = useNavigate()
return (
<div>
我是登陆页
<button onClick={() => navigate('/article')}>跳转到文章页</button>
<button onClick={() => navigate('/article?id=100&name=jack')}>searchParams传参</button>
<button onClick={() => navigate('/article/1001/jack')}>Params传参</button>
</div>
)
}
export default Login
import { useParams } from "react-router-dom"
const Article = () => {
const params = useParams()
let id = params.id
let name = params.name
return <div>我是文章页 id:{id} name:{name}</div>
}
export default Article
嵌套路由
默认二级路由配置
404路由配置
两种路由模式
项目实战技巧
配置别名路径@
路径解析配置
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件--craco
配置步骤:
- 安装craco
npm i -D @craco/craco - 项目根目录下创建配置文件
craco.config.js - 配置文件中添加路径解析配置
- 包文件中配置启动和打包命令
联想路径配置
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
}
}
数据mock
json-server实现数据mock
json-server是一个node包,可以在不到30秒内获得零编码的完整的mock服务
实现步骤:
- 项目中安装json-server
npm i -D json-server - 准备一个json文件
- 添加启动命令
- 访问接口进行测试
useReducer
useReducer-基础用法
- 定义一个reducer函数(根据不同的action返回不同的新状态)
- 在组件中调用useReducer,并传入reducer函数和状态的初始值
- 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
useReducer-分派action时传参
小结
useMemo
作用:在组件每次重新渲染的时候缓存计算的结果
背景
React的渲染机制:在React中,当组件的状态(state)或属性(props)发生变化时,React会触发组件的重新渲染,这是React的核心机制之一,确保组件能够响应数据的变化并更新UI。
可是这种渲染机制容易造成性能的浪费,组件只要有一个数据发生变化,整个组件都得重新渲染。
下面案例可以体现这一点:
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(100);
const [curTime, setCurTime] = useState('');
const useTime = () => {
useEffect(() => {
const intervalId = window.setInterval(() => {
let time = new Date();
setCurTime(time.toLocaleString());
}, 1000);
return () => {
window.clearInterval(intervalId);
};
}, []);
return curTime;
};
const time = useTime();
const arr = () => {
console.log('计算偶数数组');
const temp = [];
for (let i = 0; i < count; i++) {
if (i % 2 === 0) {
temp.push(i);
}
}
return temp;
};
console.log('组件渲染');
return (
<div>
<form>
<div>{time}</div>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={count}
onChange={(event) => {
// 设置最大值为 100000
let num = Math.min(100_000, Number(event.target.value));
setCount(num);
}}
/>
</form>
<p>
有{arr().length}个偶数在 0 到 {count} 之间:<span>{arr().join(', ')}</span>
</p>
</div>
);
}
export default App;
计算偶数数组和组件渲染日志都会间隔一秒就会打印,原因就在于curTime每秒发生了变化,导致整个组件得重新渲染,偶数就得重新计算,但是偶数的计算只依赖count值,所以造成了性能的浪费。
为了解决上述问题,useMemo应运而生。
useMemo基本介绍
useMemo(()=>{执行函数},[count1])
-
接收两个参数,第一个是执行函数,第二个是依赖项
-
把创建函数和依赖数组项作为参数传入
useMemo,它仅仅会在数组依赖项中的值改变时才会重新计算值 -
这种优化有助于避免在每次渲染时都进行高开销的计算
-
useMemo 的函数在渲染期间执行,所以不该在此期间做的操作请去除
-
如果没有提供依赖数据,每次都会重新计算值,相当于没有优化了
案例优化
import { useState, useEffect, useMemo } from 'react'
function App() {
const [count, setCount] = useState(100);
const [curTime, setCurTime] = useState('');
const useTime = () => {
useEffect(() => {
const intervalId = window.setInterval(() => {
let time = new Date();
setCurTime(time.toLocaleString());
}, 1000);
return () => {
window.clearInterval(intervalId);
};
}, []);
return curTime;
};
const time = useTime();
const arr = useMemo(() => {
console.log('useMemo: 计算偶数数组');
const temp = []
for (let i = 0; i < count; i++) {
if (i % 2 === 0) {
temp.push(i);
}
}
return temp
}, [count]);
console.log('组件渲染');
return (
<div>
<form>
<div>{time}</div>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={count}
onChange={(event) => {
// 设置最大值为 100000
let num = Math.min(100_000, Number(event.target.value));
setCount(num);
}}
/>
</form>
<p>
有{arr.length}个偶数在 0 到 {count} 之间:<span>{arr.join(', ')}</span>
</p>
</div>
);
}
export default App
加了useMemo就会发现,useMemo: 计算偶数数组这个日志只会因为count的变化而打印,不会因为整个组件的渲染而导致打印。
React.memo
作用:允许组件在Props没有改变的情况下跳过渲染
背景
React组件默认的渲染机制:只要父组件重新渲染子组件就会重新渲染。如果Son组件本身并不需要做渲染更新,就会存在浪费
只要点击父组件的按钮改变count值,我是子组件,我重新渲染了日志就会打印
import { useState } from "react";
function Son() {
console.log('我是子组件,我重新渲染了');
return <div>this is Son</div>
}
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<Son />
</div>
);
}
export default App
React.memo-基础语法
const MemoComponent=memo(function SomeComponent(props){...})
说明:经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染
使用memo进行优化
我是子组件,我重新渲染了日志只会在初始化种打印一次,点击父组件按钮不会触发子组件的重新渲染
import { useState, memo } from "react";
const MemoSon = memo(function Son() {
console.log('我是子组件,我重新渲染了');
return <div>this is Son</div>
})
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<MemoSon />
</div>
);
}
export default App
React.memo-props的比较机制
机制:在使用memo缓存组件之后,React会对每一个prop使用Object.is比较新值和旧值,返回true,表示没有变化
简单类型的prop机制我就不举例了,很容易理解。主要看一下引用类型的prop比较机制案例。
import { useState, memo } from "react";
const MemoSon = memo(function Son({ list }) {
console.log('我是子组件,我重新渲染了');
return <div>this is Son{list}</div>
})
function App() {
const [count, setCount] = useState(0)
const list = [1, 2, 3]
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<MemoSon list={list} />
</div>
);
}
export default App
按理说list就是一个固定的数组,可是当我点击父组件按钮时,我是子组件,我重新渲染了也会打印。原因在于当我点击按钮时,count值改变,父组件会重新渲染,const list = [1, 2, 3]也就会重新执行了,list的引用值就会改变,从而触发子组件的重新渲染。
所以为了解决这个问题,可以使用useMemo保证prop引用稳定,这个API可以在组件渲染的过程中缓存一个值
import { useState, memo, useMemo } from "react";
const MemoSon = memo(function Son({ list }) {
console.log('我是子组件,我重新渲染了');
return <div>this is Son{list}</div>
})
function App() {
const [count, setCount] = useState(0)
const list = useMemo(() => {
return [1, 2, 3]
}, [])
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>+{count}</button>
<MemoSon list={list} />
</div>
);
}
export default App
useCallback
使用背景
memo组件的props如果是个引用类型,上文说可以使用useMemo来包裹父组件的props保证稳定,避免子组件进行不必要的渲染。但是如果下面这个场景怎么办,这个场景本质上是一个子传父,父组件传入的是一个函数。
import { useState, memo, useMemo } from "react";
const Input = memo(function Input({ onChange }) {
console.log('子组件重新渲染了');
return <input type="text" onChange={(e) => onChange(e.target.value)} />
})
function App() {
// 传递给子组件的函数
const changeHandler = (value) => console.log(value)
// 触发父组件重新渲染的函数
const [count, setCount] = useState(0)
return (
<div className="App">
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
export default App
为了保证changeHandler稳定,需要使用useCallback
基础用法
作用:在组件多次渲染时缓存函数
const changeHandler = useCallback((value) => console.log(value), [])
这样就可以避免子组件多余的重复渲染。
React.forwardRef
使用ref暴露DOM节点给父组件
forwardRef-场景说明
默认情况父组件拿不到子组件的dom
import { forwardRef, useRef } from "react";
// 子组件
function Son() {
return <input type="text" />
}
// 父组件
function App() {
const sonRef = useRef(null)
const showRef = () => {
console.log(sonRef); //默认情况下拿不到子组件的dom
}
return (
<div className="App">
<Son ref={sonRef}></Son>
<button onClick={showRef}>focus</button>
</div>
);
}
export default App
所以就需要用到forwardRef
import { forwardRef, useRef } from "react";
// 子组件
const Son = forwardRef((props, ref) => {
return <input type="text" ref={ref} />
})
// 父组件
function App() {
const sonRef = useRef(null)
const showRef = () => {
console.log(sonRef); //默认情况下拿不到子组件的dom
sonRef.current.focus()
}
return (
<div className="App">
<Son ref={sonRef}></Son>
<button onClick={showRef}>focus</button>
</div>
);
}
export default App
useInperativeHandle
通过ref暴露子组件中的方法
场景
通过ref调用子组件内部的focus方法实现聚焦
import { forwardRef, useRef, useImperativeHandle } from "react";
// 子组件
const Son = forwardRef((props, ref) => {
// 实现聚焦逻辑
const inputRef = useRef(null)
const focusHandler = () => {
inputRef.current.focus()
}
// 把聚焦方法暴露出去
useImperativeHandle(ref, () => {
return {
focusHandler
}
})
return <input type="text" ref={inputRef} />
})
// 父组件
function App() {
const sonRef = useRef(null)
const showRef = () => {
console.log(sonRef); //暴露的方法在这里
sonRef.current.focusHandler()
}
return (
<div className="App">
<Son ref={sonRef}></Son>
<button onClick={showRef}>focus</button>
</div>
);
}
export default App
class类组件
类组件基础结构
类组件就是通过JS中的类来组织组件的代码
- 通过
类属性state定义状态数据 - 通过
setState方法来修改状态数据 - 通过
render来写UI模板(JSX语法一致)
class Counter extends Component {
// 1.定义状态变量
state = {
count: 0
}
// 2.定义事件回调修改状态数据
setCount = () => {
// 修改状态数据
this.setState({
count: this.state.count + 1
})
}
render() {
return <button onClick={this.setCount}>{this.state.count}</button>
}
}
类组件的生命周期函数
类组件的组件通信
概念:类组件和Hooks编写的组件在组件通信的思想上完全一致
zustand
后面再学