在 React 中,组件的生命周期是指组件从创建、渲染、更新到销毁的完整过程。不同阶段有不同的钩子函数(类组件)或处理方式(函数组件通过useEffect
),用于执行特定操作。下面详细说明类组件各生命周期的作用、可执行的操作,以及如何用useEffect
在函数组件中模拟这些生命周期。
一、类组件的生命周期及操作
类组件的生命周期分为挂载(Mounting) 、更新(Updating) 、卸载(Unmounting) 三个核心阶段,每个阶段有明确的钩子函数和适用场景。
1. 挂载阶段(组件从创建到插入 DOM)
挂载阶段是组件首次被创建并渲染到 DOM 的过程,仅执行一次。
钩子函数 | 执行时机 | 可执行的操作 |
---|---|---|
constructor(props) |
组件初始化时(最早执行) | - 初始化组件状态(this.state )- 绑定事件处理函数(如this.handleClick = this.handleClick.bind(this) )- 注意:避免在这里调用setState ,也不要执行副作用(如数据请求) |
static getDerivedStateFromProps(props, state) |
组件实例化后、接收新 props 时(挂载和更新阶段都会触发) | - 根据 props 动态更新 state(极少使用,仅用于 "props 直接控制 state" 的场景,如表单受控组件)- 必须返回新的 state 对象或null (不允许有副作用) |
render() |
挂载和更新阶段都会触发,用于生成 JSX | - 渲染组件 UI,返回 JSX 结构- 必须是纯函数(不修改 state、不执行副作用),仅根据 props 和 state 返回 UI |
componentDidMount() |
组件挂载到 DOM 后(最后执行) | - 执行副作用:发起网络请求(如获取初始数据)- 初始化第三方库(如图表库、地图库)- 添加 DOM 事件监听(如窗口大小变化、滚动事件)- 启动定时器(如轮询数据) |
2. 更新阶段(组件 props 或 state 变化时)
当组件的props
或state
发生变化时,会触发更新流程,可能多次执行。
钩子函数 | 执行时机 | 可执行的操作 |
---|---|---|
static getDerivedStateFromProps(props, state) |
同挂载阶段,接收新 props 后触发 | - 同挂载阶段,根据新 props 更新 state |
shouldComponentUpdate(nextProps, nextState) |
决定是否需要重新渲染(在render 前执行) |
- 返回true (默认):允许渲染;返回false :跳过渲染(性能优化)- 用于避免不必要的重渲染(如比较新旧 props/state,仅在必要时更新) |
render() |
同挂载阶段,根据新的 props/state 生成 JSX | - 重新渲染 UI(纯函数逻辑) |
getSnapshotBeforeUpdate(prevProps, prevState) |
DOM 更新前执行(在render 后、componentDidUpdate 前) |
- 捕获 DOM 更新前的快照(如滚动位置、元素尺寸)- 返回值会作为componentDidUpdate 的第三个参数 |
componentDidUpdate(prevProps, prevState, snapshot) |
DOM 更新后执行 | - 根据 props/state 变化执行副作用:- 当prevProps.id !== this.props.id 时,重新请求数据- 更新第三方库(如根据新数据刷新图表)- 恢复 DOM 快照(如滚动到之前记录的位置)- 可调用setState (但需包裹在条件判断中,避免无限循环) |
3. 卸载阶段(组件从 DOM 中移除)
仅在组件被销毁时执行一次。
钩子函数 | 执行时机 | 可执行的操作 |
---|---|---|
componentWillUnmount() |
组件卸载前执行 | - 清理副作用,避免内存泄漏:- 移除事件监听(如window.removeEventListener )- 清除定时器 / 计时器(clearInterval /clearTimeout )- 取消未完成的网络请求(如 Axios 的cancelToken )- 销毁第三方库实例(如关闭图表、地图) |
二、用useEffect
模拟生命周期(函数组件)
函数组件没有类组件的生命周期钩子,但useEffect
可以统一处理 "副作用"(如数据请求、事件监听等),通过依赖数组控制执行时机,从而模拟类组件的生命周期。
useEffect
的语法:
jsx
useEffect(() => {
// 副作用逻辑(对应类组件的"执行操作")
return () => {
// 清理函数(对应类组件的"清理操作")
};
}, [dependencies]); // 依赖数组:控制副作用的执行时机
1. 模拟componentDidMount
(挂载后执行一次)
当依赖数组为空([]
)时,副作用仅在组件挂载后执行一次,清理函数仅在卸载时执行,对应componentDidMount
和componentWillUnmount
的组合。
场景:初始化数据请求、添加全局事件监听。
jsx
import { useEffect, useState } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// 模拟componentDidMount:挂载后请求数据
const fetchUsers = async () => {
const res = await fetch('/api/users');
const data = await res.json();
setUsers(data);
};
fetchUsers();
// 模拟componentWillUnmount:卸载前清理(如取消请求)
return () => {
// 实际项目中可通过AbortController取消请求
const controller = new AbortController();
return () => controller.abort();
};
}, []); // 空依赖:仅执行一次
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. 模拟componentDidUpdate
(props/state 变化时执行)
当依赖数组包含变量(如[propA, stateB]
)时,副作用会在挂载后执行一次 ,且当依赖项变化时重新执行 ,对应componentDidUpdate
。
场景:根据 props 变化重新请求数据、更新第三方库。
jsx
import { useEffect, useState } from 'react';
function UserDetail({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 模拟componentDidUpdate:userId变化时重新请求
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUser(data);
};
fetchUser();
// 清理函数:依赖变化前执行(如取消上一次请求)
return () => {
// 取消未完成的请求,避免竞态条件
};
}, [userId]); // 依赖userId:仅当userId变化时执行
return <div>{user ? user.name : '加载中...'}</div>;
}
3. 模拟componentWillUnmount
(卸载前清理)
useEffect
的清理函数会在组件卸载前 或依赖项变化前 执行,专门用于清理副作用,对应componentWillUnmount
。
场景:移除事件监听、清除定时器。
jsx
import { useEffect, useState } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// 挂载后启动定时器
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 模拟componentWillUnmount:卸载前清除定时器
return () => {
clearInterval(timer); // 必须清理,否则组件卸载后仍会执行
};
}, []); // 空依赖:定时器只启动一次
return <div>计时:{count}秒</div>;
}
4. 模拟 "每次渲染后执行"(较少用)
若省略依赖数组(useEffect(() => { ... })
),副作用会在每次渲染后执行(包括挂载和所有更新),清理函数会在 "下一次渲染前" 执行。
场景:需要跟踪所有渲染变化的特殊场景(谨慎使用,可能导致性能问题)。
jsx
useEffect(() => {
console.log('每次渲染后执行(挂载/更新)');
return () => {
console.log('下一次渲染前执行(清理上一次副作用)');
};
}); // 无依赖数组:每次渲染都触发
三、核心区别与总结
- 类组件:基于 "生命周期阶段" 设计,钩子函数对应固定阶段,逻辑分散在多个方法中。
- 函数组件(useEffect) :基于 "副作用管理" 设计,一个
useEffect
可处理多个阶段的逻辑(如同时包含挂载和卸载的操作),更灵活。
类组件生命周期 | useEffect 模拟方式 | 核心作用 |
---|---|---|
componentDidMount |
useEffect(() => { ... }, []) |
初始化副作用(数据请求、事件监听) |
componentDidUpdate |
useEffect(() => { ... }, [dep1, dep2]) |
依赖变化时执行副作用 |
componentWillUnmount |
useEffect(() => { return () => { ... } }, []) |
清理副作用(避免内存泄漏) |
实际开发中,推荐使用函数组件 +useEffect
,因其更简洁且符合 React 的函数式编程思想。理解 "副作用" 的本质(对组件外部环境的操作),比机械对应生命周期阶段更重要。