React16生命周期流程详解及对比React15

1,647 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情

生命周期流程

虽然在16.8版本出了hook后类组件的应用被函数组件逐渐取代,但是在工作中我们还是有很多现行的类组件代码项目需要维护。因此我们还是有必要理解类组件的生命周期和避免生命周期中的坑。(本文底部有使用范例)

image-20220215150832151.png

我们可以在线查看react类组件生命周期,我们知道了16.3版本后16.4之后版本的区别主要在于getDerivedStateFromProps这个函数。

image-20220215152921449.png

挂载时

上面示例初始化页面时输出:

image-20220215193824257.png

组件初始挂载时依次触发以下生命周期函数:

1.constructor构造函数

通常在构造函数中处理两种情况:①:初始化组件内部state。②为事件处理函数绑定实例:

constructor(props) {
  super(props);
  // 不要在这里调用 this.setState()
  this.state = {
      counter: 0,
      name: this.props.name // 不要使用props.name赋值
  };
  this.handleClick = this.handleClick.bind(this);
}

此处要注意几个情况:

  • 在constructor中先调用super(props),防止在下面语句中使用this.props出现未定义的错误
  • 在使用props时,直接使用this.props,若在constructor中使用props赋值,当props更新时,不会影响state。
  • 不要再constructor中调用setState,可以直接this.state = ...赋值

2.static getDerivedStateFromProps

static getDerivedStateFromProps(props, state) {
    // 只要name变化,修改当前组件关于name的state值
    if (props.name !== state.name) {
      return {
        name: props.name
      };
    }
    return null;
}
  • getDerivedStateFromProps 会在调用 render 前调用。

  • 该方法需要return一个对象来更新state。这个对象并不会完整替换掉state,更新后,原有属性与新属性是共存的。如果返回null则不触发更新。

  • 初始挂载更新时都会被调用。

  • 该方法是静态方法,该方法内部无法访问this。

  • 派生状态会导致代码冗余,并使组件难以维护。某些情况下我们可以使用以下方法替代getDerivedStateFromProps

    • componentDidUpdate:在需要执行副作用来响应 props 中的更改(例如,数据提取或动画)是代替使用
    • memoization:依赖prop更改时重新计算某些数据
    • 使用完全受控组件:在 prop 更改时“重置”某些 state,请考虑使组件完全受控使用 key 使组件完全不受控 代替。

3.render()渲染函数

render 被调用时,它会检查 this.propsthis.state 的变化并返回以下类型之一:

  • React元素:通过JSX渲染成dom节点
  • 数组或fragments:可以使render返回多个元素
  • Portals:可以渲染子节点到不同的 DOM 子树中
  • 字符串/数值:渲染为文本节点
  • 布尔值/null:什么都不渲染

4.componentDidMount()

组件挂载后调用该函数。我们通常在componentDidMount中:①发起网络请求获取页面数据。②添加订阅/添加定时器任务等,同时需要在componentWillUnmount中取消订阅。

image-20220215163437427.png

对比

  1. 两个版本相比,少了componentWillMount生命周期函数,多了getDerivedStateFromPorps函数
  2. componentWillMount被废弃的原因:①setState:该方法再render之前调用,在此处ajax请求到数据并调用setState发生在render之前,则会导致setState无效,不会触发额外渲染。 ②Fiber原因:由于React16版本中render()之前的生命周期函数可能会被打断,所以可能会造成这些生命周期被多次执行,容易出现问题。
  3. 新增getDerivedStateFromPorps的主要原因是想要替换调componentWillReceiveProps这个生命周期函数。初始化时也会触发getDerivedStateFromPorps是为了保障这个生命周期的纯洁性,直接从命名层面约束了它的用途(从 Props 里派生 State)
  4. 另外react16中的render的返回内容新增了 数组和字符串

更新时

上面示例,点击按钮组件更新时依次触发:

image-20220215194230384.png

1.getDerivedStateFromProps()

组件更新:由父组件触发(16.3版本)

2.shoudComponentUpdate()

组件更新:组件自身的更新触发

  • shouldComponentUpdate返回一个布尔值,用来判断props或state中某些值改变时是否要触发render更新。
  • 默认情况下state变化会触发重新渲染,另外返回 false 并不会阻止子组件在 state 更改时重新渲染。
  • 首次渲染或使用 forceUpdate() 时不会调用该方法。
  • 此方法常作为性能优化方式存在。如果不希望props和state进行深层比较,可以使用PureComponent

3.render()

4. getSnapshotBeforeUpdate()

  • 该方法执行时机是render函数之后,真实dom更新之前。
  • 可以在此时获取dom更新之前的一些信息,比如滚动位置之类的。
  • 此方法可以返回值作为参数传递给componentDidUpdate。
  • 该方法不常见,可在一些类似需要特殊方式处理滚动位置的聊天线程等场景中使用。

5.componentDidUpdate()

  • 该方法在更新后立即调用
  • 组件更新后,可以在这里对dom操作
  • 也可以在这里判断props的变化进行网络请求
componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.name !== prevProps.name) {
    this.fetchData(this.props.name);
  }
}

image-20220215163404682.png

对比

  1. 16.3与16.4之后的版本中的getDerivedStateFromProps做了微调。在更新流程上:16.3版本中只有父组件的更新会触发该生命周期函数;16.4中,除了父组件更新外,setStateforceUpdate也会触发该函数。

  2. 使用getDerivedStateFromProps替换componentWillReceiveProps的原因:①父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。②此处使用setState,增加组件的重绘;若判断失效,可能导致无限重绘。

  3. getDerivedStateFromProps并不完全等同于componentWillReceiveProps替换componentWillReceiveProps的方法

    getDerivedStateFromProps 生命周期替代 componentWillReceiveProps 的背后,是 React 16 在强制推行“只用 getDerivedStateFromProps 来完成 props 到 state 的映射”这一最佳实践。确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路。

  4. 废弃的componentWillUpdate与新增的getSnapshotBeforeUpdate:①componentWillUpdate中不能调用setState,也不应进行任何操作(如dispatch等)。②getSnapshotBeforeUpdate可以返回值与componentDidUpdate通信。这个阶段可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。此外对于获取更新前滚动位置的场景,我们可以两者结合使用。

    值得一提的是,这个生命周期的设计初衷,是为了“与 componentDidUpdate 一起,涵盖过时的 componentWillUpdate 的所有用例”(引用自 React 官网)。getSnapshotBeforeUpdate 要想发挥作用,离不开 componentDidUpdate 的配合

    那么换个角度想想,为什么 componentWillUpdate 就非死不可呢?说到底,还是因为它“挡了 Fiber 的路”。

  5. 为什么一定要废弃这些生命周期?


卸载时

componentWillUnmount()

  • 在组件销毁之前调用
  • 在此方法中可以取消在componentDidMount中的一些订阅,清除timer,取消网络请求等
  • 该方法中不应再调用setState,因为此处已经是组件卸载了,不会再触发重新渲染。
  • 该方法16版本与15版本一样

生命周期中容易遇到的坑

有以下几种情况容易造成生命周期的坑

  • getDerivedStateFromProps 容易编写反模式代码,使受控组件与非受控组件区分模糊
  • componentWillMount 在 React 中已被标记弃用,不推荐使用,主要原因是新的异步渲染架构会导致它被多次调用。所以网络请求及事件绑定代码应移至 componentDidMount 中。
  • componentWillReceiveProps 同样被标记弃用,被 getDerivedStateFromProps 所取代,主要原因是性能问题
  • shouldComponentUpdate 通过返回 true 或者 false 来确定是否需要触发新的渲染。主要用于性能优化
  • componentWillUpdate 同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合 getSnapshotBeforeUpdatecomponentDidUpdate 改造使用。
  • 如果在 componentWillUnmount 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug
  • 如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加

React生命周期的两个阶段

Fiber 是 React 16 对 React 核心算法的一次重写。你只需要 get 到这一个点:Fiber 会使原本同步的渲染过程变成异步的

  1. 在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。

    同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回。这个漫长且不可打断的更新过程,将会带来用户体验层面的巨大风险:同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底完成。在这个过程中,浏览器没有办法处理任何渲染之外的事情,会进入一种无法处理用户交互的状态。因此若渲染时间稍微长一点,页面就会面临卡顿甚至卡死的风险。

  2. React 16 引入的 Fiber 架构,恰好能够解决掉这个风险:Fiber 会将一个大的更新任务拆解为许多个小任务每当执行完一个小任务时,渲染线程都会把主线程交回去,看看有没有优先级更高的工作要处理,确保不会出现其他任务被“饿死”的情况,进而避免同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是可以被打断的,这就是所谓的“异步渲染”。

换个角度看生命周期工作流

Fiber 架构的重要特征就是可以被打断的异步渲染模式。但这个“打断”是有原则的,根据“能否被打断”这一标准,React 16 的生命周期被划分为了 render 和 commit 两个阶段,而 commit 阶段又被细分为了 pre-commit 和 commit。每个阶段所涵盖的生命周期如即本文顶图,此处不再贴图

我们先来看下三个阶段各自有哪些特征

  • render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。
  • pre-commit 阶段:可以读取 DOM。
  • commit 阶段:可以使用 DOM,运行副作用,安排更新。

总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。

为什么这样设计呢?简单来说,由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,所以这个过程必须用同步渲染来求稳

使用范例:整个生命周期

import React from "react";
import ReactDOM from "react-dom";
// 定义子组件
class LifeCycleChild extends React.Component {
  // ---------------初始化---------------------
  constructor(props) {
    console.log("进入constructor");
    super(props);
    // state 可以在 constructor 里初始化
    this.state = { text: "子组件的文本" };
  }
  // 初始化/更新时调用
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps执行");
    return {
      fatherText: props.text
    }
  }
  // 初始化渲染时调用
  componentDidMount() {
    console.log("componentDidMount执行");
  }
    
  // -----------------更新----------------------
  // 组件更新时调用
  shouldComponentUpdate(prevProps, nextState) {
    console.log("shouldComponentUpdate执行");
    return true;
  }
  // 组件更新时调用
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate执行");
    return "haha";
  }
  // 组件更新后调用
  componentDidUpdate(preProps, preState, valueFromSnapshot) {
    console.log("componentDidUpdate执行");
    console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
  }
    
  // ------------------卸载---------------------
  // 组件卸载时调用
  componentWillUnmount() {
    console.log("子组件的componentWillUnmount执行");
  }
    
  // 点击按钮,修改子组件文本内容的方法
  changeText = () => {
    this.setState({
      text: "修改后的子组件文本"
    });
  };
​
  render() {
    console.log("render执行");
    return (
      <div className="container">
        <button onClick={this.changeText} className="changeText">
          修改子组件文本内容
        </button>
        <p className="textContent">{this.state.text}</p>
        <p className="fatherContent">{this.props.text}</p>
      </div>
    );
  }
}
​
// 定义 LifeCycle 组件的父组件
class LifeCycleContainer extends React.Component {
​
  // state 也可以像这样用属性声明的形式初始化
  state = {
    text: "父组件的文本",
    hideChild: false
  };
  // 点击按钮,修改父组件文本的方法
  changeText = () => {
    this.setState({
      text: "修改后的父组件文本"
    });
  };
  // 点击按钮,隐藏(卸载)LifeCycle 组件的方法
  hideChild = () => {
    this.setState({
      hideChild: true
    });
  };
  render() {
    return (
      <div className="fatherContainer">
        <button onClick={this.changeText} className="changeText">
          修改父组件文本内容
        </button>
        <button onClick={this.hideChild} className="hideChild">
          隐藏子组件
        </button>
        {this.state.hideChild ? null : <LifeCycleChild text={this.state.text} />}
      </div>
    );
  }
}
ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"));