React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)
⚠️ 重要说明:自 React 16.3 起,部分生命周期方法被标记为不安全(如
componentWillMount
,componentWillReceiveProps
,componentWillUpdate
),并在 React 17 中被移除。目前推荐使用函数组件 + Hooks(如useEffect
,useLayoutEffect
)来管理副作用和生命周期。但为了知识完整性,本文将同时介绍类组件的完整生命周期 和现代函数组件的等效写法。
一、React 类组件生命周期(完整版)
1. 挂载阶段(Mounting)
生命周期方法 | 说明 | 是否常用 |
---|---|---|
constructor() |
构造函数,初始化 state 和绑定方法 | ✅ |
static getDerivedStateFromProps() |
静态方法,根据 props 更新 state(不常用) | ⚠️ |
render() |
渲染 UI,必须实现 | ✅ |
componentDidMount() |
组件挂载后调用,适合发起网络请求、订阅事件等 | ✅ |
2. 更新阶段(Updating)
生命周期方法 | 说明 | 是否常用 |
---|---|---|
static getDerivedStateFromProps() |
同上,在 props 更新时也会调用 | ⚠️ |
shouldComponentUpdate() |
判断是否需要重新渲染,默认返回 true | ✅(优化) |
render() |
重新渲染 | ✅ |
getSnapshotBeforeUpdate() |
在 DOM 更新前获取快照(如滚动位置) | ⚠️ |
componentDidUpdate() |
DOM 更新后调用,可执行副作用操作(如更新 DOM、发请求) | ✅ |
3. 卸载阶段(Unmounting)
生命周期方法 | 说明 | 是否常用 |
---|---|---|
componentWillUnmount() |
组件卸载前调用,用于清理定时器、取消订阅等 | ✅ |
4. 错误处理阶段(Error Handling)
生命周期方法 | 说明 | 是否常用 |
---|---|---|
static getDerivedStateFromError() |
捕获子组件错误并更新 state 显示降级 UI | ✅ |
componentDidCatch() |
捕获错误并记录日志 | ✅ |
二、完整类组件生命周期案例(带详细注释)
jsx
import React from 'react';
class LifecycleDemo extends React.Component {
constructor(props) {
super(props);
console.log('[constructor] 构造函数:初始化 state 和绑定方法');
this.state = {
count: 0,
error: null
};
// 绑定方法(或使用箭头函数)
this.handleClick = this.handleClick.bind(this);
}
// ⚠️ 静态方法:根据 props 派生 state(不推荐滥用)
static getDerivedStateFromProps(props, state) {
console.log('[getDerivedStateFromProps] 根据 props 更新 state(谨慎使用)');
// 示例:如果 props.reset 为 true,则重置 count
if (props.reset && state.count !== 0) {
return { count: 0 };
}
return null; // 返回 null 表示不更新 state
}
// ✅ 组件挂载后:适合发起网络请求、订阅事件
componentDidMount() {
console.log('[componentDidMount] 组件已挂载到 DOM,可执行副作用操作');
// 模拟网络请求
this.timer = setInterval(() => {
console.log('定时器运行中...');
}, 3000);
// 模拟聚焦
this.inputRef && this.inputRef.focus();
}
// ✅ 性能优化:判断是否需要重新渲染
shouldComponentUpdate(nextProps, nextState) {
console.log('[shouldComponentUpdate] 判断是否需要更新,避免不必要的渲染');
// 示例:只有当 count 改变时才更新
if (this.state.count === nextState.count) {
return false; // 不更新
}
return true; // 更新
}
// ⚠️ DOM 更新前获取快照(如滚动位置)
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('[getSnapshotBeforeUpdate] DOM 更新前获取快照');
// 示例:获取滚动位置
if (prevState.count !== this.state.count) {
const scrollPos = window.scrollY;
return scrollPos; // 返回值会作为第三个参数传给 componentDidUpdate
}
return null;
}
// ✅ DOM 更新后:可操作 DOM 或发起请求
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('[componentDidUpdate] DOM 已更新,可执行副作用');
if (snapshot !== null) {
console.log('更新前的滚动位置:', snapshot);
}
// 示例:如果 count 改变,发送数据到服务器
if (prevState.count !== this.state.count) {
console.log(`计数更新为:${this.state.count}`);
}
}
// ✅ 组件卸载前:清理定时器、取消订阅
componentWillUnmount() {
console.log('[componentWillUnmount] 组件即将卸载,清理副作用');
if (this.timer) {
clearInterval(this.timer);
console.log('定时器已清理');
}
}
// ✅ 错误边界:捕获子组件错误
static getDerivedStateFromError(error) {
console.log('[getDerivedStateFromError] 捕获子组件错误,更新 state 显示降级 UI');
return { error: error.toString() };
}
// ✅ 错误边界:记录错误日志
componentDidCatch(error, errorInfo) {
console.log('[componentDidCatch] 捕获错误并记录日志');
console.error('错误信息:', error);
console.error('错误堆栈:', errorInfo.componentStack);
// 可以发送错误报告到服务器
}
// 事件处理函数
handleClick() {
this.setState({ count: this.state.count + 1 });
}
// 渲染函数(必须实现)
render() {
console.log('[render] 渲染 UI');
if (this.state.error) {
return <h2>发生错误:{this.state.error}</h2>;
}
return (
<div style={{ padding: '20px', border: '1px solid #ccc' }}>
<h2>React 类组件生命周期演示</h2>
<p>当前计数:{this.state.count}</p>
<button onClick={this.handleClick}>点击 +1</button>
<br /><br />
<input
ref={el => this.inputRef = el}
placeholder="自动聚焦的输入框"
style={{ marginTop: '10px' }}
/>
{/* 故意制造错误 */}
{/* <button onClick={() => { throw new Error('故意制造的错误!'); }}>
点击制造错误
</button> */}
</div>
);
}
}
export default LifecycleDemo;
三、现代 React:函数组件 + Hooks 生命周期等效写法
Hooks 对应关系:
类组件生命周期 | 函数组件 Hooks 等效写法 |
---|---|
constructor |
useState , useRef 初始化 |
componentDidMount |
useEffect(() => {}, []) |
componentDidUpdate |
useEffect(() => {}) (依赖变化时) |
componentWillUnmount |
useEffect(() => { return () => {} }, []) |
shouldComponentUpdate |
React.memo , useMemo , useCallback |
错误边界 | 仍需类组件(Hooks 无直接等效) |
函数组件完整案例:
jsx
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
function HooksLifecycleDemo({ reset }) {
const [count, setCount] = useState(0);
const [error, setError] = useState(null);
const inputRef = useRef(null);
const timerRef = useRef(null);
// ✅ 等效于 componentDidMount + componentDidUpdate + componentWillUnmount
useEffect(() => {
console.log('[useEffect] 组件挂载或 reset 变化时执行');
// 模拟 componentDidMount
timerRef.current = setInterval(() => {
console.log('定时器运行中...');
}, 3000);
// 聚焦输入框
if (inputRef.current) {
inputRef.current.focus();
}
// ✅ 等效于 componentWillUnmount:返回清理函数
return () => {
console.log('[useEffect cleanup] 组件卸载或 reset 变化前清理');
if (timerRef.current) {
clearInterval(timerRef.current);
console.log('定时器已清理');
}
};
}, [reset]); // 依赖数组:仅在 reset 变化或首次挂载时执行
// ✅ 等效于 componentDidUpdate(监听 count 变化)
useEffect(() => {
console.log('[useEffect count] count 发生变化:', count);
// 可以在这里发送数据到服务器
if (count > 0) {
console.log(`计数更新为:${count}`);
}
}, [count]); // 仅当 count 变化时执行
// ✅ 等效于 shouldComponentUpdate - 使用 useMemo 优化计算
const expensiveValue = useMemo(() => {
console.log('[useMemo] 计算昂贵值(仅在 count 变化时重新计算)');
return count * 1000;
}, [count]);
// ✅ 等效于 shouldComponentUpdate - 使用 useCallback 缓存函数
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // 依赖为空数组,函数不会重新创建
// 渲染函数
if (error) {
return <h2>发生错误:{error}</h2>;
}
return (
<div style={{ padding: '20px', border: '1px solid #ccc', marginTop: '20px' }}>
<h2>React 函数组件 + Hooks 生命周期演示</h2>
<p>当前计数:{count}</p>
<p>昂贵计算值:{expensiveValue}</p>
<button onClick={handleClick}>点击 +1</button>
<br /><br />
<input
ref={inputRef}
placeholder="自动聚焦的输入框"
style={{ marginTop: '10px' }}
/>
</div>
);
}
// 使用示例(父组件)
function App() {
const [resetFlag, setResetFlag] = useState(false);
return (
<div>
<h1>React 生命周期演示</h1>
<button onClick={() => setResetFlag(!resetFlag)}>
切换 Reset ({resetFlag ? 'true' : 'false'})
</button>
<hr />
<LifecycleDemo reset={resetFlag} />
<hr />
<HooksLifecycleDemo reset={resetFlag} />
</div>
);
}
export default App;
四、生命周期执行顺序总结
类组件挂载顺序:
constructor → getDerivedStateFromProps → render → componentDidMount
类组件更新顺序(props 或 state 变化):
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
类组件卸载顺序:
componentWillUnmount
函数组件执行顺序:
- 初始化:
useState
→useMemo
→useCallback
→render
→useEffect
(依赖为空) - 更新:
render
→useEffect
(依赖变化)→ 清理上一次 effect → 执行新的 effect - 卸载:清理所有 effect
五、最佳实践建议
- 优先使用函数组件 + Hooks:更简洁、易测试、逻辑复用方便。
- 避免使用
getDerivedStateFromProps
:除非必要,否则容易导致 bug。 - 合理使用
useEffect
依赖数组:避免无限循环或遗漏依赖。 - 及时清理副作用 :在
useEffect
返回函数中清理定时器、订阅等。 - 性能优化 :使用
React.memo
,useMemo
,useCallback
避免不必要的渲染。 - 错误边界:对于可能出错的组件,用类组件包裹提供降级 UI。
六、常见问题
Q:为什么我的 useEffect 执行了两次?
A:在开发模式下,React 会故意卸载并重新挂载组件以帮助发现清理问题。生产环境不会。
Q:如何模拟 shouldComponentUpdate?
A:使用 React.memo
包裹组件,或使用 useMemo
/useCallback
优化子组件。
Q:Hooks 能完全替代类组件吗?
A:几乎可以,除了错误边界目前仍需类组件实现。
通过以上完整案例和注释,你应该对 React 生命周期有了全面理解。在实际开发中,推荐使用函数组件 + Hooks 方案,它更符合现代 React 开发趋势!