Dark Dwarf Blog background

React 组件生命周期

React 组件生命周期

1. 组件生命周期

a.a. 基本概念

每个 React 组件都像一个生命体,从诞生(被创建并添加到 DOM)到成长(通过更新 props 和 state 来改变),再到最终的消亡(从 DOM 中移除)。React 提供了一系列特殊的“生命周期方法”,允许我们在组件生命中的特定时刻执行代码。

React 的组件生命周期的三个核心阶段如下:

  1. 挂载 (Mounting): 组件实例被创建并插入到 DOM 中的过程。
  2. 更新 (Updating): 当组件的 propsstate 发生变化,导致需要重新渲染时的过程。
  3. 卸载 (Unmounting): 当组件实例从 DOM 中被移除时的过程。

除此之外,还有一个特殊的阶段:错误处理 (Error Handling),用于捕获并处理子组件发生的错误。

下面是一个组件生命周期图谱:

alt text

b.b. 挂载阶段

当组件实例被创建并插入到 DOM 中时,以下方法会按顺序被调用。这个阶段只发生一次。

  1. constructor(props):它完成了如下工作:
    1. 通过给 this.state 赋值来初始化组件的内部 state
    2. 为事件处理函数绑定 this

注意: 这是唯一可以直接对 this.state 赋值的地方。在所有其他方法中,都必须使用 this.setState()

constructor(props) {
  super(props); // 必须先调用 super(props)
  this.state = { count: 0 };
  this.handleClick = this.handleClick.bind(this);
}
  1. static getDerivedStateFromProps(props, state):它在组件实例化后、渲染前调用,以及在后续更新中也会被调用。它允许组件根据 props 的变化来更新其内部 state。它具有如下特点:
    • 它是一个静态方法,无法访问组件实例的 this
    • 它应该返回一个对象来更新 state,或者返回 null 表示无需更新。
  • 这是一个不常用的方法,适用于 state 的值完全由 props 决定(受控组件)的罕见情况。

警告: 过度使用此方法通常会导致代码冗余和复杂化。在需要它之前,应该先考虑是否有更简单的解决方案,例如:将组件完全重构为受控组件,或使用 key prop 来重置整个组件。

  1. render()这是唯一必须的类组件方法。React 调用此方法来获取要渲染到屏幕上的 UI 描述(JSX)。render 方法必须是纯函数。这意味着在不改变 propsstate 的前提下,每次调用都应返回相同的结果,并且不应包含任何副作用(如 API 请求)。

  2. componentDidMount():它在组件已经被渲染到 DOM 中之后立即调用。它是执行需要 DOM 节点或数据加载等副作用操作的完美时机。下面这些场景会使用它:

    1. 网络请求: 从服务器获取数据。
    2. 设置订阅: 如 setIntervaladdEventListener
    3. DOM 操作: 与需要计算尺寸或位置的 DOM 元素进行交互。
componentDidMount() {
  console.log('Component has mounted!');
  const userID = this.props.userID;
  fetch(`https://api.example.com/users/${userID}`)
    .then(res => res.json())
    .then(user => this.setState({ user }));
}

c.c. 更新阶段

当组件的 propsstate 发生变化时,会触发更新。更新的方法如下:

  1. shouldComponentUpdate(nextProps, nextState):这是一个性能优化的关键方法。React 在重新渲染前会调用它,让我们有机会告诉 React 本次更新是否非必要。它的返回值如下:
    • 返回 true (默认值): 继续执行更新流程(调用 render 等)。
    • 返回 false: 跳过本次更新render() 不会被调用。

注意: 可以在此方法中比较 this.propsnextProps,以及 this.statenextState,来决定是否需要重新渲染。

为了方便,可以让组件继承自 React.PureComponent 而不是 React.ComponentPureComponent 会自动为我们实现一个 shouldComponentUpdate,它会对 propsstate 进行浅层比较

  1. render():如果 shouldComponentUpdate 返回 truerender() 方法会再次被调用,以获取最新的 UI。

  2. getSnapshotBeforeUpdate(prevProps, prevState):在 render 之后,但在最终渲染结果提交到 DOM 之前调用。它使得组件可以在 DOM 可能发生变化(例如,列表增加项目)之前从中捕获一些信息(如滚动位置)。此方法返回的任何值都将作为第三个参数传递给 componentDidUpdate()

  3. componentDidUpdate(prevProps, prevState, snapshot):在组件更新被提交到 DOM 后立即调用。此方法不适用于首次渲染。它一般用于下面的场合:

    1. props 变化时执行网络请求。
    2. 根据 DOM 的变化执行某些操作。

重要: 在 componentDidUpdate 中调用 setState 必须包裹在一个条件语句中,否则会导致无限循环的重新渲染!

componentDidUpdate(prevProps, prevState) {
  // 检查 props.userID 是否真的改变了
  if (this.props.userID !== prevProps.userID) {
    // 如果不加条件判断,每次 setState 都会触发 componentDidUpdate,造成死循环
    this.fetchData(this.props.userID);
  }
}

d.d. 卸载阶段

  1. componentWillUnmount():在组件即将被从 DOM 中移除并销毁之前调用。这是执行所有必要清理操作的理想场所。常见场景如下
    1. 清除在 componentDidMount 中创建的定时器 (clearInterval)。
    2. 移除事件监听器 (removeEventListener)。
    3. 取消网络请求或任何挂起的订阅,以防止内存泄漏。
componentWillUnmount() {
  console.log('Component will unmount. Cleaning up...');
  clearInterval(this.timerID);
  this.props.api.unsubscribe();
}

2. 在 useEffect 中的使用

函数组件中的 useEffect Hook 巧妙地将 componentDidMountcomponentDidUpdatecomponentWillUnmount 的功能整合在了一起。

  • 模拟 componentDidMount: useEffect(() => { /* ... */ }, []) (依赖项数组为空)

  • 模拟 componentDidUpdate: useEffect(() => { /* ... */ }, [dep1, dep2]) (依赖项数组有值)

  • 模拟 componentWillUnmount: useEffect 返回的清理函数。

// 函数组件
useEffect(() => {
  // 这部分代码在挂载时 (componentDidMount) 和 a, b 更新时 (componentDidUpdate) 运行
  console.log('Effect running...');

  // 返回的函数在卸载时 (componentWillUnmount) 和下一次 effect 运行前运行
  return () => {
    console.log('Cleanup...');
  };
}, [a, b]); // 依赖项数组

3. 已废弃的生命周期方法

  • UNSAFE_componentWillMount(): 在 render 之前调用。现在应使用 constructorcomponentDidMount
  • UNSAFE_componentWillReceiveProps(): 在 props 改变时调用。现在应使用 getDerivedStateFromProps
  • UNSAFE_componentWillUpdate(): 在 render 之前、shouldComponentUpdate 之后调用。现在应使用 getSnapshotBeforeUpdate