
组件通信
JSX基础—识别js表达式
在JSX中可以通过大括号语法{}识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等 1.使用引号传递字符串 2.使用JavaScript变量 3.函数调用和方法调用 4.使用JavaScript对象
注意:if语句、switch语句、变量声明属于语句,不是表达式,不能出现在}中
const count = 100;
function getName() {
return 'this is name';
}
function App() {
return (
<div className="App">
this is App
{/* 使用引号传递字符串 */}
{'this is message'}
{/* 识别js变量 */}
{count}
{/* 函数调用 */}
{getName()}
{/* 方法调用 */}
{new Date().getDate()}
{/* 使用js对象 */}
<div
style={{ color: 'red' }}>this is div
</div>
</div>
);
}
export default App;
JSX基础—实现列表渲染
const list = [
{id: 1, name: 'John Doe'},
{id: 2, name: 'Jane Smith'},
{id: 3, name: 'Alice Johnson'},
];
function App() {
return (
<div className="App">
this is App
{/*渲染列表 */}
{/*map 循环哪个结构 return结构*/}
{/*注意事项:加上一个独一无二的key字符串或者numberid*/}
{/*key的作用:React框架内部使用提升更新性能的*/}
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
JSX基础—实现点击事件
function App() {
const handClick = (name) => {
console.log('点击了'+name);
}
return (
<div className="App">
{/* 给交互事件绑定函数方法后要给函数传递参数时需要是用箭头函数避免未交互时立即调用 */}
<button onClick={()=>handClick('按钮1')}>按钮1</button>
</div>
);
}
export default App;
基础样式控制
import './span-style.css';
const spanStyle = {
color: 'red',
fontSize: "50px"
}
function App() {
return (
<div>
{/* 行内样式控制1 */}
<span style={{ color: 'red', fontSize: "50px" }}>this is span</span><br />
{/* 行内样式控制2 */}
<span style={spanStyle}>this is span</span><br />
{/* 通过class类名控制样式 */}
<span className="span-style">this is span</span>
{/* 通过class类名控制 */}
</div>
);
}
export default App;
// 下方样式文件
// span-style.css
// .span-style {
// color: 'blue',
// fontSize: "30px"
// }
classname优化类名控制
npm install classname
useState的基础使用
import { useState } from "react";
function App() {
// 1.调用useState添加一个状态变量
// count 状态变量
// setCount 修改状态变量的方法
const [count, setCount] = useState(0);
// setForm 修改复杂对象的状态变量的方法
const [form, setForm] = useState({name:'abc',age:'18'});
const handClick = () => {
console.log('点击了');
// 3.调用修改状态变量的方法
setCount(count+1);
setForm({...form,name:'def'});
}
return (
<div className="App">
{/* 2.使用状态变量(可箭头函数或直接书写) */}
<button onClick={handClick}>{count}</button>
<button onClick={handClick}>修改name为{form.name}</button>
</div>
);
}
export default App;
useReducer-基础用法
- 定义一个reducer函数(根据不同的action返回不同的新状态)
- 在组件中调用useReducer,并传入reducer函数和状态的初始值
- 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
- 在原有基础上传递额外数据因为传递的是对象,所以不限制传递字段的数量
function reducer(state, action ){
switch (action.tyoe) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'SET':
return state + action.a
default:
return state;
}
}
const [count, dispatch1] = useReducer(reducer, 0);
<h1>当前计数: {count}</h1>
<button onClick={() => dispatch1({tyoe: 'INC'})}>增加</button>
<button onClick={() => dispatch1({tyoe: 'DEC'})}>减少</button>
<button onClick={() => dispatch1({tyoe: 'SET', a: 100})}>设置为+100</button>
useEffect 的基础使用
useEffect是一个ReactHook函数,副作用函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送 AJAX请求,更改DOM等等
说明:当组件中没有发生任何的用户事件,组件渲染完毕之后就需要和服务器要数据,整个过程属于“只由渲染引起的操作”
需求:在组件渲染完毕之后,立刻从服务端获取频道列表数据并显示到页面中 语法:
useEffect(() => { } return () => {} , [])
-
参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
-
参数2是一个数组(可选参),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次,没有参数二时,组件内任何变化(组件更新)都会触发
-
return () => {} :清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行 需求:在Son组件渲染时开启一个定制器,卸载时清除这个定时器
import { useState, useEffect } 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 data = await res.json()
setList(data.data.channels)
console.log(data)
}
getList()
return () => { // useEffect一清除副作用 }
}, [])
return (
<div>
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
useMemo的基础使用
作用:在组件每次重新渲染的时候缓存计算的结果
useMemo(() => {
// 根据count1返回计算结果
}, [count1])
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算,避免因count2通过useState变化导致函数组件重绘进而触发不相干函数fib
import React, { useState,useMemo } from 'react';
function fib(n) {
console.log('触发了计算');
if (n < 3) {
return 1;
}
return fib(n - 2) + fib(n - 1);
}
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const result = useMemo(() => {
return fib(count1);
}, [count1])
console.log('组件重新渲染了');
return (
<div className="App">
<button onClick={() => setCount1(count1 + 1)}>1增加1</button>
<button onClick={() => setCount2(count2 + 1)}>2增加1</button>
<h1>当前计数: {result}</h1>
</div>
);
}
export default App;
受控表单绑定
概念:使用React组件的状态(useState)控制表单的状态
1.准备一个react状态值
const [value,setValue] = useState('')
2.通过value属性绑定状态,通过onChange属性绑定状态同步的函数
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
placeholder="请输入"
/>
3.完整步骤
import { useState } from 'react';
function App() {
const [value, setValue] = useState('');
function Input(e) {
setValue(e.target.value);
console.log(e.target.value);
}
return (
<div>
<input
type="text"
value={value}
onChange={Input}
/>
</div>
);
}
export default App;
React中获取DOM
在React 组件中获取/操作 DOM,需要使用useRef钩子函数,分为两步:
1.使用useRef创建ref 对象,并与jSX绑定
import { useState, useRef } from 'react';
const inputRef = useRef(null)
<input type="text" ref={inputRef} />
2.dom可用时,通过inputRef.current获取dom,渲染完毕之后dom生成之后才可用
console.log(inputRef.current)
import { useRef } from 'react';
function App() {
const inputRef = useRef(null)
function Input() {
console.log(inputRef.current.value); // 获取输入框的值
inputRef.current.focus(); // 获得焦点
inputRef.current.select(); // 选中输入框的所有内容
inputRef.current.blur(); // 失去焦点
}
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={Input}>获取焦点</button>
</div>
);
}
export default App;
组件通信
父传子-基础实现
概念:组件通信就是组件之间的数据传递,根据组件嵌套关系的不同,有不同的通信方法
//父传子
// 1.父组件传递数据 子组件标签身上绑定属性
// 2.子组件接收数据 props的参数;
function Son(props) {
//props:对象里面包含了父线件传递过来的所有的数据
console.log(props);
return <div>this is son ; props:{props.name}</div>;
}
function App() {
const name = "this is app";
return (
<div>
<Son name={name} />
</div>
);
}
export default App;
父传子-props说明
1.props可传递任意的数据
数字、字符串、布尔值、数组、对象、函数、JSX
子组件中得return对象有插槽的作用
<Son
name={appName}
age={20}
isTrue={false}
list={['Vue','React']}
obj={{ name: 'jack' }}
cb={() =>console.log(123)}
child={<span>this is span child</span>}
/>
2.props是只读对象
子组件只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改
父传子 - 特殊的prop children
场景:当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容
作用类似于插槽写法,传入了一个特殊对象,一个DOM节点
function Son(props) {
console.log(props);
return <div>this is son ; props-children:{props.children}</div>;
}
function App() {
return (
<div>
<Son> <span>this is span children</span></Son>
</div>
);
}
export default App;
父子组件通信 - 子传父
import { useState } from 'react';
function Son({onGetSonMsg}) {
const sonMsg = 'this is son msg'
return (
<div>
this is son
<button onClick={() => onGetSonMsg(sonMsg)}>click me</button>
</div>
)
}
function App() {
const [msg, setMsg] = useState(''); // this is app msg
const getMsg = (msg) => {
console.log(msg);
setMsg(msg);
}
return (
<div>
this.is app, {msg}
<Son onGetSonMsg={getMsg}></Son>
</div>
);
}
export default App;
使用状态提升实现兄弟组件通信
实现思路:借助“状态提升”机制,通过父组件进行兄弟组件之间的数据传递
-
A组件先通过子传父的方式把数据传给父组件App
-
App拿到数据后通过父传子的方式再传递给B组件
import { useState } from 'react';
function A({onGetAMsg,msg}) {
const aMsg = 'this is msg a';
return (
<div>
this is a --{msg}--
<button onClick={() => onGetAMsg(aMsg)}>click a me</button>
</div>
)
}
function B({onGetBMsg,msg}) {
const bMsg = 'this is msg b';
return (
<div>
this is b --{msg}--
<button onClick={() => onGetBMsg(bMsg)}>click b me</button>
</div>
)
}
function App() {
const [msg, setMsg] = useState(''); // this is app msg
const getMsg = (msg) => {
console.log(msg);
setMsg(msg);
}
return (
<div>
this.is app, {msg}----
<A onGetAMsg={getMsg} msg={msg}></A>
<B onGetBMsg={getMsg} msg={msg}></B>
</div>
);
}
export default App;
使用Context机制跨层级组件通信
实现步骤:
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件(App)中通过Ctx.Provider 组件提供数据
- 在底层组件(B)中通过 useContext 钩子函数获取消费数据
import { createContext, useContext } from 'react';
const MsgContext = createContext();
function A() {
return (
<div><B /></div>
)
}
function B() {
const msg = useContext(MsgContext); // useContext() 返回的是 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MsgContext.Provider> 的 value prop 决定。
return (
<div>{msg}----B</div>
)
}
function App() {
const msg = 'this is msg app';
return (
<div>
<MsgContext.Provider value={msg}>
<A />
</MsgContext.Provider>
</div>
);
}
export default App;
自定义Hook函数
概念:自定义Hook是以use打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用
需求:点击按钮控制相邻盒子容器的显示和隐藏
不封装直接实现 => 封装自定义Hook实现 => 抽象实现的通用逻辑
封装自定义hook通用思路
-
声明一个以use打头的函数
-
在函数体内封装可复用的逻辑(只要是可复用的逻辑)
-
把组件中用到的状态或者回调return出去(以对象或者数组)
-
在哪个组件中要用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用
ReactHooks使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
import { useState } from 'react';
function useToggle() {
// 可复用的逻辑代码
const [state, setState] = useState(false)
const toggle = () => setState(!state)
// 那些状态和操作函数需要暴露出去
return [state, toggle]
}
function App() {
const [state, toggle] = useToggle()
return (
<div>
<button onClick={toggle}>切换我</button>
<p>{state ? '是' : '否'}</p>
{state && <p>是</p>}
</div>
);
}
export default App;
什么是Redux?
Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行 作用:通过集中管理的方式管理应用的状态
Redux快速体验
不和任何框架绑定,不使用任何构建工具,使用纯Redux实现计数器
使用步骤:
- 定义一个reducer 函数(根据当前想要做的修改返回一个新的状态)
- 使用createore方选传入reducer函数生成一个store实例对象
- 使用store实例的 subscribe方法订阅数据的变化(数据一旦变化,可以得到通知)
- 使用store实例的dispatch方法提交action对象触发数据变化(告诉reducer你想怎么改数据)
- 使用store实例的 getState方法获取最新的状态数据更新到视图中
Redux管理数据流程梳理

为了职责清晰,数据流向明确,Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action和reducer
- state-一个对象存放着我们管理的数据状态
- action-一个对象用来描述你想怎么改数据
- reducer-一个函数更具action的描述生成一个新的state
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.1.1/redux.min.js"></script>
<script>
//1.定义reducer函数
// 作用:根据不同的action对象,返回不同的新的state
// 参数:state:管理的数据初始状态 action:对象type 标记当前想要做什么修改
function reducer(state = {count: 0}, action) {
// 数据不可变:不能直接修改state,只能返回新的state(状态)
switch (action.type) {
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count: state.count - 1}
default:
return state
}
}
//2.使用reducer函数生成store实例
const store = Redux.createStore(reducer)
//3.通过store实例的subscribe订阅数据变化
// 作用:当store实例中的数据发生变化时,会自动调用订阅函数
store.subscribe(() => {
const state = store.getState()
document.getElementById('count').innerHTML = state.count
})
//4.通过store实例的dispatch函数提交action更改状态
const inBtn = document.getElementById('increment')
inBtn.addEventListener('click', () => {
console.log( '增');
store.dispatch({type: 'increment'})
//增
})
const dBtn = document.getElementById('decrement')
dBtn.addEventListener('click', () => {
console.log( '减');
store.dispatch({type: 'decrement'})
//减
})
//5.通过store实例的getState方法获取最新状态更新到视图中
</script>
使用React搭配Redux
配套工具
在React中使用redux,官方要求安装俩个其他插件-ReduxToolkit和 react-redux
-
ReduxToolkit(RTK)-官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
| 简化store的配置方式 | 内置immer支持可变式状态修改 | 内置thunk更好的异步创建 | | ------------------- | --------------------------- | ----------------------- |
-
react-redux-用来链接Redux和React组件的中间件
使用React Toolkit 创建counterStore
./store/index.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./modules/counterStore";
const store = configureStore({
reducer: {
counter: counterReducer, // 配置counter模块的reducer函数
},
});
export default store;
./store/modules/counterStore.js
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
// 初始化State状态
initialState: {
count: 0,
},
// 修改状态的方法 同步方法 支持直接修改
reducers: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
},
},
});
// 解构出来actionCreater函数并导出
export const { increment, decrement } = counterSlice.actions;
// 获取reducer函数
const counterReducer = counterSlice.reducer;
// 默认导出reducer函数用于外层配置使用
export default counterReducer;
为React注入store
react-redux负责把Redux和React 链接起来,内置Provider组件通过 store 参数把创建好的store实例注入到应用中,链接正式建立
// 根组件中引入store
import store from './store';
// 引入Provider组件
import { Provider } from 'react-redux';
// 使用Provider包裹App根组件,并注入store
<Provider store={store}>
<App />
</Provider>
React组件使用store中的数据
在React组件中使用store中的数据,需要用到一个钩子函数-useSelector,它的作用是把store中的数据映射到组件中,使用样例如下:
const { count } = useSelector(state => state.counter)
// 引入useSelector钩子函数
import { useSelector, useDispatch } from 'react-redux';
// 导入store中得actionCreater函数
import { increment, decrement } from './store/modules/counterStore';
function App() {
// 从store中获取count状态
const count = useSelector(state => state.counter.count);
// 从store中获取dispatch函数
const dispatch = useDispatch();
// 渲染组件
return (
<div>
<button onClick={() => dispatch(increment())}>增加</button>
<p>当前计数: {count}</p>
<button onClick={() => dispatch(decrement())}>减少</button>
</div>
);
}
export default App;
Redux与React-提交action传参
提交action传参实现需求: 在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
即reducers对象中的函数其接收参数
- 第一个参数为store中initialState,
- 第二个参数为组件中传入的自定义参数
<button onClick={() => dispatch(increment(10))}>增加</button>
increment(state,action) {
state.count = action.payload // 10
},
异步操作样板代码
-
创建store的写法保持不变,配置好同步修改状态的方法
-
单独封装一个函数,在函数内部return一个新函数,在新函数中
-
封装异步请求获取数据
-
调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
-
组件中dispatch的写法保持不变
-
-
.组件中dispatch的写法保持不变
./store/modules/channelStore.js
import { createSlice } from "@reduxjs/toolkit";
// 引入axios
import axios from "axios";
const channelStore = createSlice({
name: "channel",
initialState: {
channelList: [],
},
reducers: {
setChannels(state, action) {
state.channelList = action.payload;
},
},
});
export const { setChannels } = channelStore.actions;
const fetchChannelList = () => {
return async (dispatch, getState) => {
console.log(getState(), '******');
const res = await axios.get("http://geek.itheima.net/v1_0/channels");
dispatch(setChannels(res.data.data.channels));
}
}
export { fetchChannelList };
// 获取reducer函数
const channelReducer = channelStore.reducer;
// 默认导出reducer函数用于外层配置使用
export default channelReducer;
组件.js
// 引入useSelector钩子函数
import { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchChannelList } from './store/modules/channelStore';
function App() {
const { channelList } = useSelector(state => state.channel);
const dispatch = useDispatch();
// 引入useEffect钩子函数
useEffect(() => {
dispatch(fetchChannelList());
}, [dispatch]);
// 渲染组件
return (
<div>
<h1>Channel List: {channelList.map(item => <li key={item.id}>{item.name}</li>)}</h1>
</div>
);
}
export default App;
路由导航
声明式导航
声明式导航是指通过在模版中通过<Link/>组件描述出要跳转到哪里去,比如后台管理系统的左侧菜单通常使用这种方式进行
<Link to="/login">登录</Link>
语法说明:通过给组件的to属性指定要跳转到路由path,组件会被渲染为浏览器支持的a链接,如果需要传参直接通过字符串拼接的方式拼接参数即可
编程式导航
编程式导航是指通过‘useNavigate`钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在登录请求完毕之后跳转就可以选择这种方式,更加灵活
import { useNavigate } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate('/about')}>跳转关于页</button>
</div>
);
}
export default Login;
语法说明:通过调用navigate方法传入地址path实现跳转
路由导航传参
searchParams 传参
navigate('/about?id=00&name=jack')
const [ params ] = useSearchParams()
let id = params.get('id')
params传参
navigate('/about/00/jack')
{
path: '/about/:id/:name?',
element: <About />,
},
const { id, name } = useParams();
嵌套路由
在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由 实现步骤:
- 使用children属性配置路由嵌套关系
- 使用
<Outlet/>组件配置二级路由渲染位置 - index: true:重定向默认渲染组件为Login
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Login />,
},
{
path: 'login',
element: <Login />,
},
{
path: 'about/:id?/:name?',
element: <About />,
},
],
{
path: "*",
element: <NotFound />,
},
},
import React from 'react';
import { Outlet, Link } from 'react-router-dom';
function Layout() {
return (
<div>
<h1>Layout</h1>
<Link to="/login">Login</Link> <br />
<Link to="/about">About</Link> <br />
<Outlet />
</div>
);
}
export default Layout;
ReactRouter -两种路由模式
各个主流框架的路由常用的路由模式有俩种,history模式和hash模式,ReactRouter分别由createBrowerRouter和 createHashRouter函数负责创建
| 路由模式 | API名称 | url表现 | 底层原理 | 是否需要后端支持 | | -------- | ------------------- | ----------- | -------------------------- | ---------------- | | history | createBrowserRouter | url/login | history对象+ pushState事件 | 需要 | | hash | createHashRouter | url/#/login | 监听 hashChange事件 | 不需要 |
路径解析配置
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件-craco 配置步骤:
-
安装craco
npm i -D @craco/craco -
项目根目录下创建配置文件craco.config.js
-
配置文件中添加路径解析配置
-
包文件中配置启动和打包命令
// craco.config.js
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}
// package.json
"scripts": {
"start": "craco start",
"build": "craco build",
}
联想路径配置
VsCode的联想配置,需要我们在项目目录下添加jsconfigjson文件,加入配置之后VsCode会自动读取配置帮助我们自动联想提示
配置步骤:
- 根目录下新增配置文件-jsconfig.json
- 添加路径提示配置
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
json-server实现数据Mock
json-server是一个node包,可以在不到30 秒内获得零编码的完整的Mock服务 实现步骤:
-
项目中安装json-server
npm i -D json-server -
准备一个json文件
-
添加启动命令
路由懒加载
- 把路由修改为由React提供的lazy函数进行动态导入
- 使用React内置的Suspense组件包裹路由中element选项对应的组件
import { lazy, Suspense } from 'react';
const Layout = lazy(()=>import('@/pages/Layout'))
// fallback:占位标签
element:<Suspense fallback={<div>loading...</div>}><Layout /></Suspense>,
打包优化-包体积分析
source-map-explorer
-
安装插件source-map-explorer
npm i source-map-explorer -D -
在package.json中配置启动命令
"scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'" }
webpack-bundle-analyzer
-
安装插件webpack-bundle-analyzer
npm i webpack-bundle-analyzer -D -
在package.json中配置启动命令
"scripts": { "analyze": "node scripts/analyze.js" } -
根目录创建scripts文件夹analyze.js文件
// 跨平台触发 bundle 分析:设置 ANALYZE 环境变量后执行 craco build const { spawn } = require('child_process'); const env = { ...process.env, ANALYZE: 'true' }; const child = spawn('craco', ['build'], { stdio: 'inherit', shell: true, env, }); child.on('close', (code) => process.exit(code ?? 0)); -
在craco.config.js配置webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { webpack: { configure: (webpackConfig) => { // 通过 ANALYZE=true 触发 bundle 分析 if (process.env.ANALYZE === 'true') { webpackConfig.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: 'server', analyzerPort: 8889, // 避开 json-server 占用的 8888 openAnalyzer: true, }) ); } return webpackConfig; }, }
useCallback
作用:在组件多次重新渲染的时候缓存函数
import { useState, memo, useCallback } from 'react';
const MemoSon = memo(function Son ({onChange}) {
console.log('MemoSon子组件重新渲染了',onChange);
return (
<input type="text" onChange={(e) => onChange(e.target.value)} />
)
});
function App() {
const handle = useCallback((value) => {console.log(value)}, []);
const [count, setCount] = useState(0);
console.log('组件重新渲染了');
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>{count}</button>
<MemoSon onChange={handle} /> <br />
</div>
);
}
export default App;
说明:使用useCallback包裹函数之后,函数可以保证在App重新渲染的时候保持引用稳定
React.memo-基础语法
const MemoComponent = memo(function SomeComponent (props){});
说明:经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染
import React, { useState, memo } from 'react';
const MemoSon = memo(function Son () {
console.log('MemoSon子组件重新渲染了');
return (
<div>son</div>
)
});
function Child () {
console.log('Child子组件重新渲染了');
return (
<div>child</div>
)
}
function App() {
const [count, setCount] = useState(0);
console.log('组件重新渲染了');
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>增加1</button>
<MemoSon />
<Child />
</div>
);
}
export default App;
React.memo - props的比较机制
机制:在使用memo缓存组件之后,React会对每一个 prop 使用Object.is比较新值和老值,全部的props比较完并返回true,表示没变化
-
prop是简单类型
Object.is(3,3)=>true 没有变化
-
prop是引用类型(对象/数组)
Object([],[]) => false 有变化,React只关心引用是否变化
import { useState, memo, useReducer, useMemo, useCallback } from 'react';
const MemoSon = memo(function Son (props) {
console.log('MemoSon子组件重新渲染了',props);
return (
<div> son {typeof props.count === 'function' ? '[Function]' : String(props.count)} </div>
)
});
function App() {
const [number1, setNumber1] = useState(100);
const [number2, setNumber2] = useReducer((state, action)=> action, [200]);
const [number3, setNumber3] = useState([300]);
let number4 = [400];
const number5 = useMemo(() => [500], []);
let number6 = 600;
// 每次渲染都创建新函数 → memo 失效
const handle7 = () => {return 700};
// useCallback 缓存函数引用 → memo 有效
const handle8 = useCallback(() => {return 800}, []);
console.log('组件重新渲染了');
return (
<div className="App">
{number1}-{number2}-{number3}-{number4}-{number5}-{number6}
<button onClick={() => setNumber1(number1 + 1)}>1增加</button>
<MemoSon count={number1} /> <br />
{/* 基本类型,值未变不会触发子组件重绘 */}
<button onClick={() => setNumber2([202])}>2增加</button>
<MemoSon count={number2} /> <br />
{/* 引用类型,引用未变不会触发;但每次点击都创建新[202],引用变了会触发 */}
<button onClick={() => setNumber3([303])}>3增加</button>
<MemoSon count={number3} /> <br />
{/* 同number2,引用类型按引用比较 */}
<MemoSon count={number4} /> <br />
{/* 每次渲染都重建数组引用,必然触发子组件重绘 */}
<MemoSon count={number5} /> <br />
{/* useMemo空依赖缓存引用,不会触发子组件重绘 */}
<MemoSon count={number6} /> <br />
{/* 基本类型,值相同不会触发子组件重绘 */}
<MemoSon count={handle7} /> <br />
{/* 每次渲染创建新函数引用,必然触发子组件重绘 */}
<MemoSon count={handle8} /> <br />
{/* useCallback缓存函数引用,不会触发子组件重绘 */}
</div>
);
}
export default App;
总结:非响应式对象会因为组件重绘而导致子组件重绘,非响应式的基础数据类型因值未变不会导致子组件重绘
forwardRef-基础使用
获取子组件ref:父组件通过ref获取到子组件内部的元素并进行相关操作(给子组件传递ref给想要操作的节点进行绑定)
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.current.value);
}
return (
<div className="App">
<button onClick={showRef}>获取子组件值</button>
<Son ref={sonRef}/>
</div>
);
}
export default App;
useImperativeHandle-基础使用
调用子组件方法:父组件通过ref调用子组件内部的封装方法实现相操作
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 focusHandler = () => {
sonRef.current.focusHandler();
}
return (
<div className="App">
<button onClick={focusHandler}>获取子组件值</button>
<Son ref={sonRef}/>
</div>
);
}
export default App;
类组件基础结构
类组件就是通过JS中的类来组织组件的代码
- 通过类属性state定义状态数据
- 通过setState方法来修改状态数据
- 通过render来写UI模版(JSX语法一致)
//编写一个类组件
import React, { Component } from 'react'
class Counter extends Component {
// 定义状态管理
state = {
count: 0
}
// 自定义方法
setCount = (count) => {
// 修改状态数据
this.setState(
{ count: this.state.count + 1 }
)
}
render() {
return (
<div>
<h1>当前计数: {this.state.count}</h1>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>增加1</button>
</div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return (
<div>
<Counter />
<h1>当前计数: {this.state.count}</h1>
<button onClick={() => this.setState({ count: this.state.count + 2 })}>增加2</button>
</div>
)
}
}
export default App;
类组件的生命周期函数
概念:组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数

- componentDidMount:组件挂载完毕自动执行-异步数据获取
- componentWillUnmount:组件卸载时自动执行-清理副作用
dangerouslySetInnerHTML
是 React 提供的一个特殊属性,用于在组件中直接将 HTML 字符串插入到 DOM 中。它的出现,是为了替代原生 JavaScript 中的 innerHTML 方法。
它的语法有些特殊,必须传入一个包含 __html 键的对象:
function MyComponent() {
const htmlContent = "<strong>粗体文本</strong> 和 <em>斜体文本</span>";
return (
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
);
}
- 注意:
__html是固定的键名(双下划线开头),值必须是字符串。 - 使用该属性的标签内部不能有任何子元素,否则 React 会报错。
-
必须进行净化 (Sanitize): 如果内容来自用户输入(如评论、富文本编辑器),必须使用专门的库过滤掉危险标签,然后再传给
dangerouslySetInnerHTML。import DOMPurify from 'dompurify'; function SafeComponent({ userInput }) { // 使用 DOMPurify 净化 HTML,移除 <script>、onerror 等危险内容 const cleanHtml = DOMPurify.sanitize(userInput); return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />; } -
替代方案: 如果只是展示普通文本或换行,优先使用
textContent或结合white-space: pre-wrapCSS,不要为了省事而用dangerouslySetInnerHTML
开屏动画(Splash Screen)的防闪屏优化
核心原理是通过 sessionStorage 标记 + CSS 类名切换,避免用户在页面间导航时重复播放开屏动画。
实现原理
1. 执行时机
<Script id="handle-splash-logic" strategy="beforeInteractive">
{`
try {
if (sessionStorage.getItem('hasSeenSplash') === 'true') {
document.documentElement.classList.add('splash-seen');
}
} catch (e) {}
`}
</Script>
使用 strategy="beforeInteractive" ,确保这段 JS 在 React 水合和页面可交互之前就执行,从而在浏览器渲染内容前就决定好是否显示开屏动画。
2. 工作流程
用户首次访问 → sessionStorage 无标记 → 不加 splash-seen 类 → 显示开屏动画 ↓ 动画结束写入 hasSeenSplash=true ↓ 用户导航/刷新 → sessionStorage 有标记 → 加 splash-seen 类 → 跳过动画直接显示内容
3. 三段代码的配合关系
第一段 - CSS 隐藏规则
<style
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `
#app-mount-root { opacity: 0; visibility: hidden; pointer-events: none; }
html.splash-seen #app-mount-root { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; }
`
}}
/>
第二段 - JS 标记检测
// 页面加载最早期检查:用户本次会话是否已看过开屏动画
if (sessionStorage.getItem('hasSeenSplash') === 'true') {
document.documentElement.classList.add('splash-seen');
// → 触发上面的 CSS,内容直接可见,不播动画
}
第三段 - SplashScreen 组件写入标记 (动画播放完毕后):
// 开屏动画播完后,写入标记
sessionStorage.setItem('hasSeenSplash', 'true');
4. 为什么用 sessionStorage 而不是 localStorage
| 特性 | sessionStorage | localStorage | | -------- | ------------------------ | ------------ | | 生命周期 | 关闭标签页后清除 | 永久保留 | | 适用场景 | 本次浏览会话只看一次开屏 | 永久不再看 |
用 sessionStorage 的效果是: 用户关闭标签页再打开时会再看一次开屏动画 ,但在同一会话内页面间导航不会重复播放,体验最好。
5. suppressHydrationWarning 的作用
layout.tsx#L35 和 L39 上的 suppressHydrationWarning 是因为这段 JS 会修改 的 classList (添加 splash-seen ),导致服务端渲染的 HTML 和客户端 DOM 不一致。React 水合时检测到这个差异会报 warning,加这个属性告诉 React 忽略此处的差异。
6. try-catch 的防御作用
try { ... } catch (e) {}
防止无痕模式或禁用 sessionStorage 的浏览器环境抛错导致整页白屏