声明:GLM4.5生成,辅助复习
简洁简洁
- 使用简洁的大白话同时结合案例,帮我复习React知识之数据管理 简洁简洁
复习React数据管理,咱们用“过日子”的比喻来串,保证简单易懂!
一、组件自己的“小本本”:State(状态)
大白话
每个组件就像一个独立的人,有自己的“小本本”(state),记着属于自己的事(比如计数器点了多少次、输入框打了什么字)。这个小本本只有自己能改,改了之后自己会“重新打扮”(重新渲染),展示新样子。
核心工具
useState:给组件开个“小本本”,格式是const [数据, 改数据的函数] = useState(初始值)。
案例:计数器
import { useState } from 'react';
function Counter() {
// 开个小本本,记“count”,初始值0
const [count, setCount] = useState(0);
return (
<div>
<p>你点了 {count} 次</p>
{/* 点按钮就改小本本里的count,加1 */}
<button onClick={() => setCount(count + 1)}>点我+1</button>
</div>
);
}
关键点:
count是“小本本”里的数据,setCount是改数据的笔(只有用这笔改,组件才会重新渲染)。- 直接改
count(比如count++)没用!必须用setCount,React才会知道“数据变了,你该更新了”。
二、父母给孩子的“零花钱”:Props(属性)
大白话
父组件像父母,子组件像孩子。父母可以把“零花钱”(数据)通过props给孩子,孩子只能花(用),不能改(props是只读的)。如果孩子想改零花钱,得让父母用自己的“小本本”(state)改,再重新给孩子。
案例:父组件传名字给子组件
// 子组件(孩子):接收父母给的“name”零花钱
function Child({ name }) {
return <p>我叫 {name}</p>;
}
// 父组件(父母):有自己的“小本本”记名字,传给孩子
function Parent() {
const [childName, setChildName] = useState("小明");
return (
<div>
{/* 把childName通过props(name)传给Child */}
<Child name={childName} />
{/* 父母改自己小本本,孩子的“零花钱”自动更新 */}
<button onClick={() => setChildName("小红")}>改孩子名字</button>
</div>
);
}
关键点:
- 子组件通过
props(这里是{ name }解构)收数据,只能读,不能直接改(比如name = "小红"会报错)。 - 数据流向:父组件state → props → 子组件。子组件想改数据,得“通知”父组件改state(比如父组件传个函数给子组件,子组件调用这个函数)。
三、家族的“传家宝”:Context(跨组件共享数据)
大白话
如果家族很大(比如爷爷→爸爸→孙子→重孙),爷爷想把“传家宝”(数据)直接给重孙,不用一层层传(props drilling),就用Context。Context就像“家族公告栏”,爷爷把传家宝放上去,所有后代都能直接拿。
核心工具
createContext(创建公告栏)、Provider(放数据的人)、useContext(拿数据的人)。
案例:主题切换(爷爷直接给重孙传主题)
import { createContext, useContext, useState } from 'react';
// 1. 创建“家族公告栏”(ThemeContext),默认值是“light”
const ThemeContext = createContext("light");
// 重孙组件:直接从公告栏拿主题
function GreatGrandchild() {
const theme = useContext(ThemeContext); // 拿公告栏的数据
return <p>当前主题:{theme}</p>;
}
// 爷爷组件:用Provider把主题放公告栏,所有后代都能拿
function Grandpa() {
const [theme, setTheme] = useState("light");
return (
// ThemeContext.Provider:给公告栏放数据,value是“传家宝”
<ThemeContext.Provider value={theme}>
<div>
<GreatGrandchild />
<button onClick={() => setTheme("dark")}>切换暗黑主题</button>
</div>
</ThemeContext.Provider>
);
}
关键点:
- Context解决“props层层传”的麻烦,适合全局共享的数据(比如主题、用户登录状态)。
- 用
Provider包裹组件,通过value传数据;后代用useContext直接拿,不用中间组件传props。
四、公司的“总部”:Redux(复杂状态管理)
大白话
如果应用很大(比如电商网站),有很多组件需要共享数据(购物车、用户信息等),用Context会很乱(比如多个公告栏,不知道听谁的)。Redux就像“公司总部”,统一管理所有数据(store),组件要改数据,得“打报告”(dispatch action),总部根据报告(action)找对应部门(reducer)改数据,改完通知所有组件“数据更新了”。
核心概念
Store:总部仓库,存所有数据。Action:报告(比如“添加商品到购物车”),格式是{ type: "ADD_CART", payload: 商品信息 }。Reducer:部门(比如“购物车部门”),根据action报告改store里的数据(纯函数,输入旧数据+action,输出新数据)。Dispatch:打报告的按钮,组件用dispatch(action)告诉总部“我要改数据”。
案例:购物车(Redux管理)
1. 安装Redux:npm install @reduxjs/toolkit react-redux
2. 创建“购物车部门”(reducer)和总部(store)
// cartSlice.js(用Redux Toolkit简化写法)
import { createSlice } from '@reduxjs/toolkit';
// 创建购物车“部门”
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] }, // 初始数据:空购物车
reducers: {
// 添加商品到购物车的“报告处理方式”
addItem: (state, action) => {
state.items.push(action.payload); // 直接改state(Toolkit内部会处理成不可变更新)
},
// 删除商品的“报告处理方式”
removeItem: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
}
}
});
// 导出“打报告”的函数(addItem, removeItem)
export const { addItem, removeItem } = cartSlice.actions;
// 导出“部门”(reducer),给总部用
export default cartSlice.reducer;
3. 创建“总部”(store)
// store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
// 创建总部,把“购物车部门”挂进去
const store = configureStore({
reducer: {
cart: cartReducer // 购物车数据归cartReducer管
}
});
export default store;
4. 组件用“总部数据”和“打报告”
// App.js(用Provider包裹整个应用,让所有组件能访问总部)
import { Provider } from 'react-redux';
import store from './store';
import Cart from './Cart';
function App() {
return (
<Provider store={store}>
<Cart />
</Provider>
);
}
// Cart.js(购物车组件)
import { useSelector, useDispatch } from 'react-redux';
import { addItem, removeItem } from './cartSlice';
function Cart() {
// 从总部拿购物车数据(useSelector相当于“去总部查数据”)
const cartItems = useSelector(state => state.cart.items);
// 拿“打报告”的按钮(dispatch)
const dispatch = useDispatch();
const itemToAdd = { id: 1, name: "苹果", price: 5 };
return (
<div>
<h2>购物车</h2>
<ul>
{cartItems.map(item => (
<li key={item.id}>
{item.name} - {item.price}元
<button onClick={() => dispatch(removeItem(item.id))}>删除</button>
</li>
))}
</ul>
{/* 点按钮就打“添加商品”的报告 */}
<button onClick={() => dispatch(addItem(itemToAdd))}>添加苹果</button>
</div>
);
}
关键点:
- Redux适合“多组件共享复杂数据”的场景(比如购物车、用户权限、全局配置)。
- 数据流向:组件
dispatch(action)→store调用reducer→reducer改store数据 → 组件通过useSelector自动更新。
五、从“服务器”拿数据:React Query(服务端状态管理)
大白话
前面说的state、props、Context、Redux都是管“客户端数据”(比如用户输入、购物车)。但如果数据来自服务器(比如API接口),用它们会很麻烦(比如手动处理加载中、错误、缓存)。React Query就像“快递员”,专门管“服务器数据”:帮你取数据、存缓存、自动更新(比如数据过期了重新取),还能显示“快递在路上”(加载中)、“快递丢了”(错误)。
核心工具
useQuery:取数据(查询),useMutation:改数据(增删改)。
案例:获取用户列表(显示加载中、错误、数据)
1. 安装:npm install @tanstack/react-query
2. 用Provider包裹应用(给快递员“工作范围”)
// App.js
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import UserList from './UserList';
// 创建“快递员总部”(QueryClient)
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
3. 组件用useQuery取数据
// UserList.js
import { useQuery } from '@tanstack/react-query';
// 模拟从服务器取数据的函数(实际用fetch/axios)
const fetchUsers = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
return res.json();
};
function UserList() {
// 用useQuery取数据,参数:[数据唯一标识, 取数据函数]
const { data, isLoading, error } = useQuery({
queryKey: ['users'], // 数据的唯一标识(比如“用户列表”)
queryFn: fetchUsers // 取数据的函数
});
if (isLoading) return <p>加载中...</p>; // 快递在路上
if (error) return <p>出错了:{error.message}</p>; // 快递丢了
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
关键点:
- React Query专门管“服务器数据”,自动处理缓存(比如第二次取同一数据直接用缓存)、加载状态、错误状态。
useQuery用于“查数据”(GET请求),useMutation用于“改数据”(POST/PUT/DELETE请求,比如添加用户后自动刷新列表)。
总结:什么时候用什么?
| 场景 | 工具 | 比喻 |
|---|---|---|
| 组件自己记数据 | useState | 自己的小本本 |
| 父子传数据 | props | 父母给孩子的零花钱 |
| 跨多层组件共享数据 | Context | 家族公告栏(传家宝) |
| 大应用复杂状态管理 | Redux | 公司总部(统一管理) |
| 服务器数据(API) | React Query | 快递员(取、存、自动更新) |
记住:简单用state/props,跨组件用Context,复杂状态用Redux,服务器数据用React Query,数据管理就清清楚楚啦!
- 使用简洁的大白话同时结合案例,讲解React setState 调用的原理,React setState 调用之后发生了什么?是同步还是异步?React中的setState批量更新的过程是什么?React中有使用过getDefaultProps吗?它有什么作用?React中setState的第二个参数作用是什么?React中的setState和replaceState的区别是什么?在React中组件的this.state和setState有什么区别?state 是怎么注入到组件的,从 reducer 到组件经历了什么样的过程React组件的state和props有什么区别?React中的props为什么是只读的?在React中组件的props改变时更新组件的有哪些方法?React中怎么检验props?验证props的目的是什么?
好的,咱们用大白话+生活案例,把React状态管理这些“老大难”问题一次性讲透!
一、setState 调用原理 & 调用后发生了什么?同步还是异步?
大白话原理
setState 就像你给 React 状态更新中心 打电话说:“嘿,我的状态要变了!新值是XXX!”
React 不会立刻帮你改(异步),而是先记在小本本(更新队列)上。等它忙完手头的事(比如处理事件、渲染组件),再一次性把所有记录的状态更新处理掉(批量更新),最后重新渲染组件(“刷个新界面”)。
调用后发生了什么?(分步说)
- 你打电话(调用
setState):this.setState({ count: 1 }); // 告诉React:count要改成1 - React 记账(加入队列):
React 把{ count: 1 }这个更新请求塞进一个叫 “更新队列” 的口袋里(不立刻执行)。 - React 找时机批量处理:
等 React 当前任务忙完(比如点击事件处理完),它会打开口袋,把里面所有更新请求 合并处理(比如你连续调了3次setState,它可能合并成一次)。 - 计算新状态 & 重新渲染:
React 用合并后的新状态替换旧状态,然后触发组件的render()方法,生成新界面(“刷个屏”)。
同步还是异步?
- 异步(大部分情况):
在 React 事件(如onClick)、生命周期函数中调用setState,是异步的(React 要攒一起批量处理)。
案例:handleClick() { this.setState({ count: 1 }); // 异步:先记账,不立刻改 console.log(this.state.count); // 输出 0(还是旧值!) this.setState({ count: 2 }); // 再记账 console.log(this.state.count); // 输出 0(还是旧值!) // React 事件结束后,批量处理:count = 2,然后重新渲染 } - 同步(特殊情况):
在setTimeout、Promise.then()、原生 DOM 事件等 React 控制不到 的地方调用,React 会立刻处理(同步)。
案例:handleClick() { setTimeout(() => { this.setState({ count: 1 }); // 同步:立刻改! console.log(this.state.count); // 输出 1(新值!) }, 0); }
二、setState 批量更新的过程是什么?
大白话过程
批量更新就像 食堂打饭:
- 你排队喊:“我要一份饭!”(
setState) - 食堂阿姨(React)不立刻打,而是继续问后面的人:“你要什么?”(继续记账)
- 等队伍没人了(当前任务结束),阿姨一次性把所有人的饭打完(批量处理更新),然后叫大家取餐(重新渲染)。
技术过程
- React 进入“批量模式”:
在事件处理、生命周期开始时,React 开启一个“批量更新开关”(isBatchingUpdates = true)。 - 收集更新请求:
所有setState调用不会立即执行,而是被推入 更新队列(dirtyComponents)。 - 关闭“批量模式” & 执行更新:
事件/生命周期结束时,React 关闭开关(isBatchingUpdates = false),然后:- 合并队列中的多个更新(比如
{ count: 1 }和{ count: 2 }合并成{ count: 2 })。 - 用合并后的新状态调用
shouldComponentUpdate(判断是否需要更新)。 - 如果需要更新,调用
render()生成新 Virtual DOM,对比后更新真实 DOM。
- 合并队列中的多个更新(比如
三、React 中有使用过 getDefaultProps 吗?它有什么作用?
大白话
getDefaultProps 是 老版本 React(16.3 前) 给组件设置 默认 props 的方法,就像给手机设置“默认壁纸”:如果用户没传壁纸,就用默认的。
作用
- 为 props 设置默认值,避免因未传 props 导致报错(如
undefined调用方法)。 - 案例(老写法):
class MyComponent extends React.Component { static getDefaultProps() { return { name: "默认用户" }; // 如果没传 name,就用这个 } render() { return <div>你好,{this.props.name}</div>; } } // 使用时:<MyComponent /> → 显示“你好,默认用户”
现在怎么写?
React 16.3+ 推荐用 静态属性 defaultProps:
class MyComponent extends React.Component {
static defaultProps = {
name: "默认用户" // 效果一样,更简洁
};
// 或函数组件:
// MyComponent.defaultProps = { name: "默认用户" };
}
四、setState 的第二个参数作用是什么?
大白话
第二个参数是 回调函数,就像你告诉快递员:“送完货给我打个电话!”——React 在 状态更新完成、界面渲染后,会自动调用这个函数。
作用
- 在状态更新后执行操作(比如发请求、操作 DOM)。
- 案例:
this.setState( { count: 1 }, // 第一个参数:新状态 () => { // 第二个参数:回调函数 console.log("状态更新完成!新值是:", this.state.count); // 输出 1 document.getElementById("myDiv").scrollIntoView(); // 滚动到某个元素 } );
注意
现在更推荐用 useEffect 替代(函数组件):
useEffect(() => {
console.log("count 更新了:", count); // 依赖 count,更新后执行
}, [count]);
五、setState 和 replaceState 的区别?
大白话
setState:修改 状态(像在笔记本上 改一行字,其他行保留)。replaceState:替换 整个状态(像 撕掉旧笔记本,换本新的,旧状态全丢)。
区别对比
| 特点 | setState | replaceState(已废弃) |
|---|---|---|
| 操作方式 | 合并旧状态({ ...oldState, ...newState }) | 直接覆盖旧状态(丢弃未更新的属性) |
| 案例 | this.setState({ a: 1 }) → 保留 b, c | this.replaceState({ a: 1 }) → 只剩 a |
| 现状 | 正在使用 | React 16+ 已废弃,用 setState 替代 |
为什么废弃 replaceState?
容易误操作导致状态丢失(比如不小心把其他状态属性删了),setState 的合并方式更安全。
六、组件的 this.state 和 setState 有什么区别?
大白话
this.state:当前状态(像你钱包里的 现金,可以直接看,但别直接改!)。setState:修改状态的工具(像 银行柜台,改钱必须通过它,不然会出问题)。
核心区别
| 特点 | this.state | setState |
|---|---|---|
| 作用 | 读取当前状态 | 修改状态并触发更新 |
| 能否直接修改 | ❌ 绝对禁止!(React 不会知道你改了) | ✅ 唯一合法修改方式 |
| 案例 | console.log(this.state.count) | this.setState({ count: 1 }) |
| 后果 | 直接改 → 界面不更新,数据混乱 | 正确触发更新流程 |
七、state 是怎么注入到组件的?从 reducer 到组件的过程(以 Redux 为例)
大白话流程
Redux 的 state 像公司 总部仓库,组件像 门店:
- 仓库(Store):存所有 state(用
createStore创建)。 - 门店申请(Connect):组件用
connect(或useSelector)告诉总部:“我要这些数据!” - 总部发货(Provider):
Provider组件把仓库“快递服务”覆盖到整个应用。 - 门店收货(State 注入):组件通过
props(或useSelector返回值)收到 state,就能用了。
技术流程
// 1. 创建仓库(Store)
const store = createStore(reducer); // reducer 是“仓库管理员”
// 2. 用 Provider 包裹应用(提供快递服务)
<Provider store={store}>
<App />
</Provider>
// 3. 组件连接仓库(门店申请)
class MyComponent extends React.Component {
render() {
// 4. state 通过 props 注入到组件!
return <div>{this.props.count}</div>; // count 来自仓库的 state
}
}
// 用 connect 连接组件和仓库
const mapStateToProps = (state) => ({
count: state.count // 从仓库取 count,注入到组件的 props
});
export default connect(mapStateToProps)(MyComponent);
函数组件用 useSelector 更简单:
function MyComponent() {
const count = useSelector(state => state.count); // 直接从仓库取数据
return <div>{count}</div>;
}
八、组件的 state 和 props 有什么区别?
大白话
- state:组件 自己的钱(自己管,自己改,改了自己刷新)。
- props:别人 给你的钱(只能花,不能改,想改得让主人改完再给你)。
核心区别
| 特点 | State | Props |
|---|---|---|
| 来源 | 组件内部自己定义(this.state) | 父组件传递(<Child name="小明" />) |
| 可修改性 | ✅ 组件自己用 setState 修改 | ❌ 只读(子组件不能改) |
| 作用 | 管理组件内部动态数据(如输入框内容) | 父子组件通信(传递配置、数据) |
| 案例 | 计数器的 count | 父组件传给子组件的 用户名 |
九、React 中的 props 为什么是只读的?
大白话
props 是 父母给孩子的零花钱,孩子如果自己乱改(比如把 10 元改成 100 元),会导致:
- 父母不知道:父母以为孩子只有 10 元,实际孩子有 100 元(数据不一致)。
- 家庭混乱:其他亲戚(兄弟组件)也以为孩子有 10 元,结果用错了(数据流混乱)。
React 强制 props 只读,是为了 保证数据单向流动(父→子),让数据流向清晰可预测。
技术原因
- 单向数据流:React 的核心设计,数据从父组件流向子组件,子组件改 props 会破坏这个规则。
- 可维护性:如果子组件能改 props,调试时很难追踪数据变化(“谁改了它?”)。
十、props 改变时更新组件的方法
大白话
props 变了,就像别人给了你新零花钱,你要 重新算账(更新组件)。React 默认会帮你更新,但你可以“插手”决定更不更新。
方法
- 默认更新(React 自动):
父组件传新 props → 子组件自动render()(大部分情况够用)。 - 手动控制更新(
shouldComponentUpdate):
在类组件中,通过shouldComponentUpdate决定是否更新。
案例:只有props.name变了才更新:shouldComponentUpdate(nextProps) { return nextProps.name !== this.props.name; // name变了才返回true } - 性能优化(
React.PureComponent/React.memo):PureComponent(类组件):自动浅比较 props 和 state,没变就不更新。React.memo(函数组件):同上,包裹函数组件即可。
案例:
function MyComponent(props) { /* ... */ } export default React.memo(MyComponent); // props没变就不重新渲染
十一、怎么检验 props?验证 props 的目的是什么?
大白话
检验 props 就像 检查快递包裹:
- 目的:防止收到“假货”(错误类型的数据),避免组件报错(比如传数字却传了字符串)。
检验方法
- PropTypes(推荐):
React 官方库,专门用来检查 props 类型。
安装:npm install prop-types
案例:import PropTypes from 'prop-types'; function MyComponent({ name, age }) { return <div>{name} - {age}</div>; } MyComponent.propTypes = { name: PropTypes.string.isRequired, // name必须是字符串,必传 age: PropTypes.number // age必须是数字 }; // 如果传 <MyComponent name={123} />,控制台会报错! - TypeScript(更强大):
在 TS 中直接定义 props 类型,编译时就能报错。
案例:interface Props { name: string; age?: number; // ?表示可选 } function MyComponent({ name, age }: Props) { // ... } // <MyComponent name={123} /> → 编译时报错!
验证 props 的目的
- 防错:避免因 props 类型错误导致组件崩溃(如
undefined.length)。 - 文档:
PropTypes相当于组件的“说明书”,告诉其他开发者怎么用。 - 调试:控制台警告能快速定位 props 传错的地方。
总结一张表
| 问题点 | 核心答案 |
|---|---|
| setState 原理 | 异步记账 → 批量处理 → 重新渲染 |
| 同步/异步 | 事件中异步,setTimeout/Promise 中同步 |
| 批量更新 | React 开启批量模式 → 收集更新 → 合并处理 → 渲染 |
| getDefaultProps | 老版本设置 props 默认值,现用 defaultProps 静态属性 |
| setState 第二个参数 | 状态更新后的回调函数(现多用 useEffect 替代) |
| setState vs replaceState | setState 合并状态,replaceState 替换状态(已废弃) |
| state vs setState | state 是当前状态(只读),setState 是修改状态的唯一方式 |
| state 注入流程(Redux) | Store → Provider → connect/useSelector → 组件 props/state |
| state vs props | state 是组件自己的数据(可改),props 是父组件传的数据(只读) |
| props 只读原因 | 保证单向数据流,避免数据混乱 |
| props 改变更新方法 | 默认自动更新,可用 shouldComponentUpdate、PureComponent、React.memo 控制 |
| props 检验方法 | PropTypes(运行时检查)或 TypeScript(编译时检查) |
| props 检验目的 | 防错、文档化、方便调试 |
用大白话讲完这些,React 状态管理是不是清晰多了?记住核心:state 是自己的,props 是别人的;改状态用 setState,别瞎动 props;批量更新是 React 的“懒人优化”,PropTypes 是“防错神器”!
- 使用简洁的大白话同时结合案例,帮我复习React知识之生命周期
好的,咱们用“人的一生”来比喻React组件的生命周期,结合大白话和实际案例,保证你一看就懂!
一、生命周期是什么?
组件从“出生”(创建)→ “成长”(更新)→ “死亡”(卸载)的整个过程,React在不同阶段提供“钩子函数”(生命周期方法),让你在合适的时机做该做的事(比如出生时起名字、成长时换衣服、死亡时写遗书)。
二、生命周期三大阶段(类比人的一生)
1. 挂载阶段(Mounting)——组件“出生”
流程:怀孕(创建)→ 出生(插入DOM)→ 办出生证(初始化完成)
钩子函数顺序:
constructor → getDerivedStateFromProps → render → componentDidMount
(1)constructor(怀孕期)
- 大白话:组件“怀上了”,初始化“基因”(state)和“本能”(绑定方法)。
- 作用:
- 初始化state(
this.state = { ... }) - 绑定事件处理函数(
this.handleClick = this.handleClick.bind(this))
- 初始化state(
- 案例:
class Baby extends React.Component { constructor(props) { super(props); // 必须写!相当于“继承父母的基因” this.state = { name: "小明", age: 0 }; // 初始化state(出生时的名字和年龄) this.cry = this.cry.bind(this); // 绑定“哭”这个本能 } cry() { console.log("哇哇哇~"); } }
(2)render(出生)
- 大白话:组件“出生”了,第一次“亮相”(渲染到DOM上)。
- 作用:
- 必须实现! 返回要显示的JSX(就像婴儿出生后第一次被看到的样子)。
- 不能改state! 纯粹的“展示”,不能有副作用(比如不能在这里发网络请求)。
- 案例:
render() { return ( <div> <p>我叫{this.state.name},今年{this.state.age}岁</p> <button onClick={this.cry}>哭一下</button> </div> ); }
(3)componentDidMount(办出生证)
- 大白话:组件“出生”后,医院给它办出生证(DOM操作、发请求、定时器等“正式工作”)。
- 作用:
- 操作DOM(比如获取元素尺寸、添加事件监听)
- 发网络请求(比如从服务器获取数据)
- 启动定时器(比如倒计时)
- 案例:从服务器获取用户信息
componentDidMount() { // 发请求获取用户数据(相当于“上户口”) fetch("https://api.example.com/user") .then(res => res.json()) .then(data => { this.setState({ name: data.name, age: data.age }); // 更新state }); // 启动定时器(比如“成长倒计时”) this.timer = setInterval(() => { this.setState(prevState => ({ age: prevState.age + 1 })); }, 1000); }
2. 更新阶段(Updating)——组件“成长”
触发条件:
- 父组件传了新props(比如父母给孩子换了新衣服)
- 自己调用了
setState(比如孩子自己长高了)
流程:检查成长需求 → 决定是否成长 → 长个子(重新渲染)→ 成长后调整
钩子函数顺序:
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
(1)getDerivedStateFromProps(检查成长需求)
- 大白话:父母(props)给孩子新衣服,孩子先检查“这件衣服我能不能穿?”(根据props更新state)。
- 作用:
- 静态方法(不能访问
this),接收props和state,返回新state或null。 - 少用! 大部分场景用其他方法替代,容易导致逻辑复杂。
- 静态方法(不能访问
- 案例:props变时同步更新state
static getDerivedStateFromProps(props, state) { // 如果父组件传了新name,更新state的name if (props.name !== state.name) { return { name: props.name }; } return null; // 不更新state }
(2)shouldComponentUpdate(决定是否成长)
- 大白话:孩子问自己:“我真的需要长个子吗?”(避免不必要的重新渲染)。
- 作用:
- 返回
true(更新)或false(不更新),默认true。 - 性能优化神器! 比如props没变时就不重新渲染。
- 返回
- 案例:只有年龄变化时才更新
shouldComponentUpdate(nextProps, nextState) { // 只有年龄变了才更新(名字变了不更新) return nextState.age !== this.state.age; }
(3)render(长个子)
- 大白话:孩子“长个子”了(重新渲染),展示新样子。
- 注意:和挂载阶段的
render一样,必须实现且不能改state!
(4)getSnapshotBeforeUpdate(成长前拍照)
- 大白话:孩子长个子前,先拍张照片(记录更新前的DOM信息,比如滚动位置)。
- 作用:
- 在DOM更新前调用,返回值会传给
componentDidUpdate。 - 场景:处理滚动位置、DOM尺寸等(比如聊天窗口更新前记住滚动条位置)。
- 在DOM更新前调用,返回值会传给
- 案例:聊天窗口更新前记住滚动位置
getSnapshotBeforeUpdate(prevProps, prevState) { // 如果消息列表更新了,记住当前滚动位置 if (this.props.list.length !== prevProps.list.length) { return this.listRef.scrollHeight; // 返回滚动高度 } return null; }
(5)componentDidUpdate(成长后调整)
- 大白话:孩子长个子后,调整衣服(操作DOM、发请求等)。
- 作用:
- 接收3个参数:
prevProps(旧props)、prevState(旧state)、snapshot(getSnapshotBeforeUpdate的返回值)。 - 注意:如果要调用
setState,必须加条件判断,否则会死循环!
- 接收3个参数:
- 案例:聊天窗口更新后恢复滚动位置
componentDidUpdate(prevProps, prevState, snapshot) { // 如果消息列表更新了,恢复滚动位置 if (this.props.list.length !== prevProps.list.length && snapshot !== null) { this.listRef.scrollTop = snapshot; // 恢复滚动位置 } }
3. 卸载阶段(Unmounting)——组件“死亡”
流程:写遗书(清理工作)→ 永别(从DOM移除)
钩子函数:componentWillUnmount
componentWillUnmount(写遗书)
- 大白话:组件“死亡”前,写遗书(清理定时器、取消订阅、移除事件监听等)。
- 作用:
- 清理副作用! 避免内存泄漏(比如定时器没关会一直运行)。
- 案例:清除定时器和事件监听
componentWillUnmount() { // 清除定时器(避免“死了还在长个子”) clearInterval(this.timer); // 移除事件监听(避免“死了还在听消息”) window.removeEventListener("resize", this.handleResize); }
三、旧版生命周期(已废弃,了解即可)
React 16.3前有一些生命周期方法,现在标记为UNSAFE_(不安全),因为容易导致问题:
componentWillMount(挂载前):现在用componentDidMount替代(避免服务端渲染不一致)。componentWillReceiveProps(props更新前):现在用getDerivedStateFromProps替代(避免多次渲染)。componentWillUpdate(更新前):现在用getSnapshotBeforeUpdate+componentDidUpdate替代(避免DOM操作时机错误)。
四、函数组件的生命周期(Hooks模拟)
函数组件没有生命周期方法,但用useEffect可以模拟:
useEffect(() => { ... }, []):模拟componentDidMount(挂载后执行)。useEffect(() => { ... return () => { ... } }, [dep]):模拟componentDidUpdate+componentWillUnmount(依赖变化时执行,卸载时清理)。
案例:函数组件模拟生命周期
import { useState, useEffect } from "react";
function FunctionBaby() {
const [age, setAge] = useState(0);
// 模拟componentDidMount(挂载后执行)
useEffect(() => {
console.log("组件挂载了!");
const timer = setInterval(() => {
setAge(prev => prev + 1);
}, 1000);
// 模拟componentWillUnmount(卸载时清理)
return () => {
clearInterval(timer);
console.log("组件卸载了!");
};
}, []); // 空依赖,只在挂载/卸载时执行
// 模拟componentDidUpdate(age变化时执行)
useEffect(() => {
console.log("年龄更新了:", age);
}, [age]); // 依赖age
return <p>今年{age}岁</p>;
}
五、生命周期总结表
| 阶段 | 钩子函数 | 大白话比喻 | 作用/案例 |
|---|---|---|---|
| 挂载 | constructor | 怀孕期 | 初始化state、绑定方法 |
render | 出生 | 第一次渲染JSX | |
componentDidMount | 办出生证 | 发请求、操作DOM、启动定时器 | |
| 更新 | getDerivedStateFromProps | 检查成长需求 | 根据props更新state(少用) |
shouldComponentUpdate | 决定是否成长 | 性能优化(避免不必要渲染) | |
render | 长个子 | 重新渲染JSX | |
getSnapshotBeforeUpdate | 成长前拍照 | 记录DOM信息(如滚动位置) | |
componentDidUpdate | 成长后调整 | 操作DOM、发请求(加条件判断) | |
| 卸载 | componentWillUnmount | 写遗书 | 清理定时器、取消订阅、移除事件监听 |
六、经典面试题
Q1:为什么在componentDidMount里发请求,不在constructor或render里?
constructor:组件还没挂载到DOM,发请求可能导致数据回来时组件还没“出生”,报错。render:不能有副作用(发请求会阻塞渲染,且可能触发多次渲染导致重复发请求)。componentDidMount:组件已挂载,DOM可用,发请求最安全。
Q2:shouldComponentUpdate和React.memo有什么区别?
shouldComponentUpdate:类组件用,手动控制是否更新(返回true/false)。React.memo:函数组件用,自动浅比较props,没变就不更新(相当于函数版的shouldComponentUpdate)。
Q3:componentWillUnmount里不清理定时器会怎么样?
定时器会一直运行,即使组件卸载了,导致内存泄漏(页面卡顿,甚至崩溃)。就像人死了,心脏还在跳,吓人!
最后口诀
挂载:constructor → render → DidMount
更新:DerivedState → ShouldUpdate → render → Snapshot → DidUpdate
卸载:WillUnmount(清理要记牢)
函数组件用useEffect,依赖数组别忘掉!
用生命周期,就像带孩子,该出生时出生,该成长时成长,该死亡时写好遗书——React组件的一生,清清楚楚!
- 使用简洁的大白话同时结合案例,讲解React的生命周期有哪些? React 废弃了哪些生命周期?为什么?React 16.X 中 props 改变后在哪个生命周期中处理React 性能优化在哪个生命周期?它优化的原理是什么?state 和 props 触发更新的生命周期分别有什么区别?React中发起网络请求应该在哪个生命周期中进行?为什么?React 16中新生命周期有哪些
用“人的一生”比喻React生命周期,结合案例和原理,帮你彻底搞懂!
一、React生命周期有哪些?(类比人的一生)
1. 挂载阶段(Mounting)——组件“出生”
| 钩子函数 | 大白话比喻 | 作用/案例 |
|---|---|---|
constructor | 怀孕期(初始化基因) | 初始化state、绑定方法:this.state = { name: "小明" } |
render | 出生(第一次亮相) | 渲染JSX:return <div>{this.state.name}</div> |
componentDidMount | 办出生证(正式工作) | 发请求、操作DOM、启动定时器:fetch("/api/user") |
2. 更新阶段(Updating)——组件“成长”
| 钩子函数 | 大白话比喻 | 作用/案例 |
|---|---|---|
getDerivedStateFromProps | 检查新衣服(props变时更新state) | 少用!props同步state:if (props.name !== state.name) return { name: props.name } |
shouldComponentUpdate | 决定是否长个子(优化性能) | 控制是否更新:return nextProps.age !== this.state.age |
render | 长个子(重新渲染) | 同上,必须实现 |
getSnapshotBeforeUpdate | 成长前拍照(记录DOM信息) | 保存滚动位置:return this.list.scrollHeight |
componentDidUpdate | 成长后调整(处理副作用) | 操作DOM/发请求(加条件判断):if (prevProps.id !== this.props.id) fetchData() |
3. 卸载阶段(Unmounting)——组件“死亡”
| 钩子函数 | 大白话比喻 | 作用/案例 |
|---|---|---|
componentWillUnmount | 写遗书(清理工作) | 清除定时器/事件监听:clearInterval(this.timer) |
二、废弃了哪些生命周期?为什么?
废弃的3个生命周期(标记为UNSAFE_)
| 废弃钩子 | 替代方案 | 废弃原因(大白话) |
|---|---|---|
componentWillMount | componentDidMount | 出生前(挂载前)做事太早: - 服务端渲染时可能执行两次 - 发请求可能阻塞渲染 |
componentWillReceiveProps | getDerivedStateFromProps | 收到新props时做事太随意: - 容易导致无限循环setState - 与异步渲染冲突 |
componentWillUpdate | getSnapshotBeforeUpdate + componentDidUpdate | 更新前做事太危险: - DOM还没更新就操作,可能出错 - 与新异步渲染机制冲突 |
核心原因:React 16引入“异步渲染”
- 旧问题:同步渲染中,
componentWill*钩子可能导致渲染中断(比如在componentWillUpdate里setState会卡死)。 - 新方案:用
getDerivedStateFromProps(静态)和getSnapshotBeforeUpdate(DOM更新前)替代,确保渲染流程可控。
三、React 16.X 中 props 改变后在哪个生命周期处理?
答案:getDerivedStateFromProps 或 componentDidUpdate
| 场景 | 推荐生命周期 | 案例 |
|---|---|---|
| props变 → 更新state | getDerivedStateFromProps | 父组件传新name,子组件同步更新:static getDerivedStateFromProps(props, state) {<br/> if (props.name !== state.name) {<br/> return { name: props.name };<br/> }<br/> return null;<br/>} |
| props变 → 副作用操作 | componentDidUpdate | props变后发请求:componentDidUpdate(prevProps) {<br/> if (prevProps.id !== this.props.id) {<br/> fetch(`/api/user/${this.props.id}`);<br/> }<br/>} |
注意:优先用
componentDidUpdate!getDerivedStateFromProps是“静态方法”(不能访问this),容易导致逻辑混乱,官方建议少用。
四、React性能优化在哪个生命周期?原理是什么?
优化生命周期:shouldComponentUpdate
- 作用:决定组件是否需要更新(返回
true更新,false不更新)。 - 原理:避免不必要的渲染(比如props没变时跳过
render和DOM操作)。
案例:只有age变化时才更新
class User extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// 只有age变化才更新(name变化不更新)
return nextState.age !== this.state.age;
}
render() {
console.log("组件渲染了!"); // 只有age变时才打印
return <div>{this.state.name} - {this.state.age}</div>;
}
}
进阶优化方案
| 方案 | 适用组件 | 原理 |
|---|---|---|
React.PureComponent | 类组件 | 自动浅比较props和state(相当于内置shouldComponentUpdate) |
React.memo | 函数组件 | 自动浅比较props(相当于函数版PureComponent) |
浅比较原理:只比较第一层数据(如
props.a、state.b),不比较嵌套对象(如props.user.name)。复杂对象需手动优化。
五、state 和 props 触发更新的生命周期区别
| 触发条件 | 生命周期流程 | 关键区别 |
|---|---|---|
| setState | getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate | 可控性强:可在shouldComponentUpdate中拦截更新,优化性能。 |
| props变 | 同上(但getDerivedStateFromProps必触发) | 被动更新:子组件无法阻止props更新(除非用shouldComponentUpdate)。 |
案例对比
// 父组件
class Parent extends React.Component {
render() {
return <Child name="小明" />; // 传props给子组件
}
}
// 子组件
class Child extends React.Component {
state = { age: 10 };
// props变时触发
static getDerivedStateFromProps(props, state) {
console.log("props变了,但我不一定更新state");
return null;
}
// setState时触发
handleClick = () => {
this.setState({ age: 11 }); // 触发state更新
};
// 优化:只有state.age变才更新
shouldComponentUpdate(nextProps, nextState) {
return nextState.age !== this.state.age;
}
render() {
return <button onClick={this.handleClick}>点我更新age</button>;
}
}
六、网络请求应该在哪个生命周期?为什么?
唯一答案:componentDidMount
为什么?
- 安全:组件已挂载到DOM,操作DOM或更新state不会报错。
- 只执行一次:避免重复请求(如在
render或componentWillUpdate中请求,每次渲染都发)。 - 兼容服务端渲染:
componentWillMount在服务端会执行,但服务端没有window/fetch,会报错。
错误示范
// ❌ 错误:在constructor中请求(组件未挂载)
constructor(props) {
super(props);
fetch("/api/user"); // 可能导致数据回来时组件还没“出生”
}
// ❌ 错误:在render中请求(每次渲染都发)
render() {
fetch("/api/user"); // 无限循环!
return <div>...</div>;
}
正确案例
class UserList extends React.Component {
state = { users: [] };
componentDidMount() {
// ✅ 正确:挂载后发请求
fetch("/api/users")
.then(res => res.json())
.then(users => this.setState({ users }));
}
render() {
return (
<ul>
{this.state.users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
}
七、React 16新生命周期有哪些?
新增的2个生命周期
| 新钩子 | 作用 | 案例 |
|---|---|---|
getDerivedStateFromProps | 在render前调用,根据props返回新state(静态方法) | props变时同步state:static getDerivedStateFromProps(props) {<br/> return { name: props.name };<br/>} |
getSnapshotBeforeUpdate | 在DOM更新前调用,返回值传给componentDidUpdate(保存DOM信息) | 保存滚动位置:getSnapshotBeforeUpdate() {<br/> return this.list.scrollHeight;<br/>} |
废弃的3个旧生命周期(见第二部分)
componentWillMount→ 替代:componentDidMountcomponentWillReceiveProps→ 替代:getDerivedStateFromProps+componentDidUpdatecomponentWillUpdate→ 替代:getSnapshotBeforeUpdate+componentDidUpdate
八、总结:生命周期核心口诀
挂载:constructor → render → DidMount
更新:DerivedState → ShouldUpdate → render → Snapshot → DidUpdate
卸载:WillUnmount(清理要记牢)
废弃:WillMount/WillReceiveProps/WillUpdate(异步渲染惹的祸)
请求:DidMount(安全又可靠)
优化:ShouldUpdate/PureComponent/memo(浅比较防浪费)
用生命周期就像带孩子:该出生时出生,该成长时成长,该死亡时写好遗书——React组件的一生,清清楚楚!