React 生命周期演变
对比 React 旧版 Will 系列与新版生命周期方法
问题
React 16.3 废弃了 componentWillMount、componentWillReceiveProps、componentWillUpdate 三个生命周期方法,引入了 getDerivedStateFromProps 和 getSnapshotBeforeUpdate。为什么要这样改?新旧方法如何对应?
解答
旧版生命周期(已废弃)
class OldLifecycle extends React.Component {
// 组件挂载前调用,在 render 之前
componentWillMount() {
// 问题:在这里发起异步请求,可能导致多次渲染
// 在 SSR 中会执行两次
}
// 接收新 props 时调用
componentWillReceiveProps(nextProps) {
// 问题:父组件重新渲染就会触发,即使 props 没变
if (nextProps.id !== this.props.id) {
this.setState({ data: null });
this.fetchData(nextProps.id);
}
}
// 更新前调用
componentWillUpdate(nextProps, nextState) {
// 问题:在 Fiber 架构下可能被多次调用
// 读取 DOM 信息可能不准确
}
}
废弃原因
React Fiber 引入了异步渲染,render 阶段可能被中断、暂停、重新执行。Will 系列方法在 render 阶段执行,存在以下问题:
- 可能被多次调用:异步渲染下,render 阶段的方法可能执行多次
- 副作用不安全:在这些方法中发起请求、订阅事件会导致重复执行
- DOM 读取不可靠:
componentWillUpdate读取的 DOM 状态可能与最终提交时不一致
新版生命周期
class NewLifecycle extends React.Component {
constructor(props) {
super(props);
this.state = {
prevId: props.id,
data: null
};
this.listRef = React.createRef();
}
// 静态方法,在 render 前调用
// 返回值用于更新 state,返回 null 表示不更新
static getDerivedStateFromProps(nextProps, prevState) {
// 替代 componentWillReceiveProps
// 注意:无法访问 this,无法执行副作用
if (nextProps.id !== prevState.prevId) {
return {
prevId: nextProps.id,
data: null // 标记需要重新获取数据
};
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 数据获取移到这里
if (this.state.data === null) {
this.fetchData(this.props.id);
}
// snapshot 来自 getSnapshotBeforeUpdate
if (snapshot !== null) {
this.listRef.current.scrollTop +=
this.listRef.current.scrollHeight - snapshot;
}
}
// 在 DOM 更新前调用,返回值传给 componentDidUpdate
getSnapshotBeforeUpdate(prevProps, prevState) {
// 替代 componentWillUpdate 中的 DOM 读取
// 在 commit 阶段调用,保证 DOM 读取准确
if (prevProps.list.length < this.props.list.length) {
return this.listRef.current.scrollHeight;
}
return null;
}
render() {
return <div ref={this.listRef}>{/* ... */}</div>;
}
}
生命周期对照表
| 旧版方法 | 新版替代方案 |
|---|---|
componentWillMount | constructor + componentDidMount |
componentWillReceiveProps | getDerivedStateFromProps + componentDidUpdate |
componentWillUpdate | getSnapshotBeforeUpdate + componentDidUpdate |
完整生命周期顺序
// 挂载阶段
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
// 更新阶段
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
// 卸载阶段
componentWillUnmount()
迁移示例:滚动位置保持
// 旧版写法(有问题)
class OldList extends React.Component {
componentWillUpdate(nextProps) {
if (nextProps.items.length > this.props.items.length) {
// 问题:读取的 scrollHeight 可能不准确
this.previousScrollHeight = this.listRef.scrollHeight;
}
}
componentDidUpdate() {
if (this.previousScrollHeight) {
// 保持滚动位置
this.listRef.scrollTop +=
this.listRef.scrollHeight - this.previousScrollHeight;
}
}
}
// 新版写法(推荐)
class NewList extends React.Component {
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.items.length < this.props.items.length) {
// 在 DOM 更新前读取,保证准确
return this.listRef.current.scrollHeight;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.listRef.current.scrollTop +=
this.listRef.current.scrollHeight - snapshot;
}
}
}
关键点
- 废弃原因:Fiber 异步渲染下,render 阶段方法可能被多次调用,副作用不安全
- getDerivedStateFromProps 是静态方法,无法访问 this,强制避免副作用
- getSnapshotBeforeUpdate 在 commit 阶段调用,保证 DOM 读取准确
- 副作用迁移:数据获取、订阅等操作应放在
componentDidMount和componentDidUpdate中 - 渐进迁移:旧方法加
UNSAFE_前缀仍可使用,但会在 React 18 严格模式下警告
目录