React 生命周期演变

对比 React 旧版 Will 系列与新版生命周期方法

问题

React 16.3 废弃了 componentWillMountcomponentWillReceivePropscomponentWillUpdate 三个生命周期方法,引入了 getDerivedStateFromPropsgetSnapshotBeforeUpdate。为什么要这样改?新旧方法如何对应?

解答

旧版生命周期(已废弃)

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 阶段执行,存在以下问题:

  1. 可能被多次调用:异步渲染下,render 阶段的方法可能执行多次
  2. 副作用不安全:在这些方法中发起请求、订阅事件会导致重复执行
  3. 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>;
  }
}

生命周期对照表

旧版方法新版替代方案
componentWillMountconstructor + componentDidMount
componentWillReceivePropsgetDerivedStateFromProps + componentDidUpdate
componentWillUpdategetSnapshotBeforeUpdate + 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 读取准确
  • 副作用迁移:数据获取、订阅等操作应放在 componentDidMountcomponentDidUpdate
  • 渐进迁移:旧方法加 UNSAFE_ 前缀仍可使用,但会在 React 18 严格模式下警告