核心分界线:React 16.3(引入 Fiber 架构)
核心变化概览
变化可概括为:废弃 3 个 will 钩子,新增 2 个钩子,保留核心流程不变。
| 变更类型 | 旧版钩子 | 新版替代 | 说明 |
|---|---|---|---|
| 废弃 | componentWillMount |
--- | 挂载前可能执行多次,不安全 |
| 废弃 | componentWillReceiveProps |
getDerivedStateFromProps |
用静态方法替代,避免副作用 |
| 废弃 | componentWillUpdate |
getSnapshotBeforeUpdate |
更新前 DOM 快照捕获 |
| 新增 | --- | getDerivedStateFromProps |
静态方法,根据 props 派生 state |
| 新增 | --- | getSnapshotBeforeUpdate |
获取更新前 DOM 状态,返回值传给 componentDidUpdate |
| 保留 | constructor |
constructor |
初始化状态 |
| 保留 | render |
render |
渲染 UI |
| 保留 | shouldComponentUpdate |
shouldComponentUpdate |
性能优化阀门 |
| 保留 | componentDidMount |
componentDidMount |
挂载完成,发请求、订阅 |
| 保留 | componentDidUpdate |
componentDidUpdate(prevProps, prevState, snapshot) |
更新完成,新增 snapshot 参数 |
| 保留 | componentWillUnmount |
componentWillUnmount |
清理工作 |
💡 一句话总结 :旧版的
componentWillMount、componentWillReceiveProps、componentWillUpdate被废弃,取而代之的是getDerivedStateFromProps和getSnapshotBeforeUpdate。其余生命周期钩子基本保持不变。
旧版生命周期(React 16.3 之前)
挂载阶段
kotlin
constructor → componentWillMount → render → componentDidMount
更新阶段
| 触发方式 | 执行顺序 |
|---|---|
setState() |
shouldComponentUpdate → componentWillUpdate → render → componentDidUpdate |
props 变化 |
componentWillReceiveProps → shouldComponentUpdate → componentWillUpdate → render → componentDidUpdate |
forceUpdate() |
跳过 shouldComponentUpdate,直接 componentWillUpdate → render → componentDidUpdate |
卸载阶段
componentWillUnmount
新版生命周期(React 16.3+)
挂载阶段
kotlin
constructor → getDerivedStateFromProps → render → componentDidMount
更新阶段
scss
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate(prevProps, prevState, snapshot)
卸载阶段
componentWillUnmount
新旧生命周期对比总表
| 生命周期 | 状态 | 旧版行为 | 新版行为 |
|---|---|---|---|
constructor |
保留 | 初始化 state,绑定事件处理器 | 功能不变 |
componentWillMount |
废弃 | 组件挂载前调用,可执行副作用 | 移除。副作用移至 componentDidMount |
componentWillReceiveProps |
废弃 | props 变化时调用,可执行副作用和 setState |
替换为 getDerivedStateFromProps(静态方法,无 this) |
getDerivedStateFromProps |
新增 | --- | 静态方法,接收 props 和 state,返回对象更新 state 或 null |
shouldComponentUpdate |
保留 | 返回 false 阻止更新 |
功能不变,性能优化阀门 |
componentWillUpdate |
废弃 | 更新前调用,可读取 DOM 状态 | 替换为 getSnapshotBeforeUpdate |
getSnapshotBeforeUpdate |
新增 | --- | 更新前获取 DOM 快照,返回值传给 componentDidUpdate |
render |
保留 | 返回 JSX / null | 功能不变 |
componentDidMount |
保留 | 挂载完成,发请求、订阅 | 功能不变 |
componentDidUpdate |
保留 | 接收 prevProps、prevState |
新增第三个参数 snapshot(来自 getSnapshotBeforeUpdate) |
componentWillUnmount |
保留 | 清理定时器、取消订阅 | 完全不变 |
为什么废弃 3 个 will 钩子?
⚠️ 根本原因:Fiber 架构的异步渲染
React 16 引入 Fiber Reconciler,将渲染拆分为可中断、可恢复的小任务单元。这导致 will 系列钩子在异步渲染中可能被多次调用,产生不可预期的副作用。
Stack Reconciler vs Fiber Reconciler
| 特性 | Stack Reconciler(旧版) | Fiber Reconciler(新版) |
|---|---|---|
| 渲染方式 | 深度优先递归同步渲染 | 将渲染拆分为小任务单元 |
| 可中断性 | 一旦开始无法中断 | 可中断、可恢复、可优先级调度 |
will 钩子执行次数 |
只执行一次,行为确定 | 可能被执行多次 |
具体问题场景
重复副作用问题 :假设在 componentWillMount 中发起 API 请求,如果渲染被中断后重新开始,请求会被重复发送,造成资源浪费和数据不一致。
javascript
// ❌ 危险:componentWillMount 中发请求
componentWillMount() {
fetch('/api/data').then(res => {
this.setState({ data: res.data });
});
// 如果渲染中断重启,请求会被重复发送!
}
javascript
// ✅ 正确:componentDidMount 中发请求
componentDidMount() {
fetch('/api/data').then(res => {
this.setState({ data: res.data });
});
// 只在挂载完成时执行一次,安全
}
Fiber 架构与生命周期变更
React 团队的决策逻辑
| 废弃的钩子 | 在 Fiber 中的问题 | 解决方案 |
|---|---|---|
componentWillMount |
可能被多次调用,副作用不可控 | 移除,副作用移至 componentDidMount |
componentWillReceiveProps |
可执行 setState 和副作用,在 Fiber 中行为不可预期 |
替换为纯函数 getDerivedStateFromProps |
componentWillUpdate |
可读取 DOM,但 Fiber 中 DOM 可能尚未准备好 | 替换为 getSnapshotBeforeUpdate 获取快照 |
💡 设计原则 :React 团队将生命周期分为渲染阶段 (Render Phase)和提交阶段 (Commit Phase)。
will钩子位于渲染阶段,可能被多次调用;did钩子位于提交阶段,保证只执行一次。
getDerivedStateFromProps 详解
基本用法
javascript
class Example extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
// 纯函数:不能访问 this
// 根据 props 派生 state
if (nextProps.userId !== prevState.prevUserId) {
return {
prevUserId: nextProps.userId,
userData: null // 触发重新加载
};
}
return null; // 不更新 state
}
state = {
prevUserId: null,
userData: null
};
}
与 componentWillReceiveProps 对比
🔴 componentWillReceiveProps(旧版)
javascript
componentWillReceiveProps(nextProps) {
// ❌ 可以访问 this
// ❌ 可以执行副作用
// ❌ 可以 setState
if (nextProps.id !== this.props.id) {
this.setState({ loading: true });
fetchData(nextProps.id); // 副作用!
}
}
🟢 getDerivedStateFromProps(新版)
javascript
static getDerivedStateFromProps(nextProps, prevState) {
// ✅ 纯函数,无 this
// ✅ 无副作用
// ✅ 只能返回对象或 null
if (nextProps.id !== prevState.prevId) {
return { prevId: nextProps.id, loading: true };
}
return null;
}
⚠️ 使用建议 :
getDerivedStateFromProps容易导致代码复杂化。官方建议优先考虑以下替代方案:
- 完全受控组件:用 props 直接控制,不使用 state
- 完全非受控组件 :使用
key属性重置组件状态- 使用 Hooks :函数组件中
useEffect更灵活
getSnapshotBeforeUpdate 详解
典型场景:保存滚动位置
javascript
class ChatList extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
// 更新前记录列表底部距离
if (prevProps.messages.length < this.props.messages.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 更新后恢复滚动位置
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
}
执行时机
render()输出新的虚拟 DOMgetSnapshotBeforeUpdate()在 DOM 更新前执行,可读取当前 DOM 状态- React 将变更提交到真实 DOM
componentDidUpdate(prevProps, prevState, snapshot)接收快照值
Hooks 替代方案(React 16.8+)
随着 Hooks 的引入,函数组件可以完全替代类组件的生命周期管理。
| 类组件生命周期 | 函数组件 Hook | 说明 |
|---|---|---|
constructor |
useState / useReducer |
初始化状态 |
componentDidMount |
useEffect(() => {}, []) |
空依赖数组,只执行一次 |
componentDidUpdate |
useEffect(() => {}, [deps]) |
指定依赖数组 |
componentWillUnmount |
useEffect(() => { return () => {} }, []) |
返回清理函数 |
shouldComponentUpdate |
React.memo + useMemo / useCallback |
性能优化 |
getDerivedStateFromProps |
直接在组件体中计算派生值 | 或使用 useMemo |
getSnapshotBeforeUpdate |
自定义 Hook + ref | 需手动实现 |
Hook 示例
javascript
function Example({ userId }) {
const [userData, setUserData] = useState(null);
// 替代 componentDidMount + componentDidUpdate
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) setUserData(data);
});
// 替代 componentWillUnmount
return () => { cancelled = true; };
}, [userId]); // 依赖变化时重新执行
return <div>{userData?.name}</div>;
}
最佳实践
1. 副作用放在正确的位置
| 场景 | 推荐钩子 | 不推荐 |
|---|---|---|
| 初始化发请求、订阅 | componentDidMount |
componentWillMount(废弃) |
| props 变化时同步 state | getDerivedStateFromProps(谨慎) |
componentWillReceiveProps(废弃) |
| 更新前保存 DOM 信息 | getSnapshotBeforeUpdate |
componentWillUpdate(废弃) |
| 性能优化(避免不必要渲染) | shouldComponentUpdate / React.memo |
--- |
| 清理定时器、取消订阅 | componentWillUnmount |
--- |
2. 避免常见陷阱
🚫 不要在
getDerivedStateFromProps中执行副作用这是一个静态方法,无法访问
this,也不能发请求或操作 DOM。它应该只用于根据 props 计算并返回新的 state。
⚠️ 谨慎使用派生 state大多数
getDerivedStateFromProps的使用场景都可以用更好的方式替代。在引入它之前,先问自己:这个 state 真的需要派生吗?
迁移建议
逐步迁移策略
迁移检查清单:
- 第一步 :识别代码中使用的
UNSAFE_前缀方法(如UNSAFE_componentWillMount) - 第二步 :将
componentWillMount中的逻辑移至componentDidMount - 第三步 :将
componentWillReceiveProps替换为getDerivedStateFromProps或重构为完全受控/非受控组件 - 第四步 :将
componentWillUpdate替换为getSnapshotBeforeUpdate - 第五步(可选):考虑将类组件重构为函数组件 + Hooks
使用 codemod 自动迁移
bash
# React 官方提供的 codemod,自动重命名废弃的生命周期方法
npx react-codemod rename-unsafe-lifecycles
💡 长期建议 :对于新项目,优先使用函数组件 + Hooks。类组件的生命周期概念在维护老代码时仍然重要,但新开发应拥抱 Hooks 带来的更简洁、更可组合的状态管理方案。