理解一下React生命周期函数

1,259 阅读10分钟

生命周期总览

projects.wojtekmaj.pl/react-lifec… image.png

黄色框的生命周期是在React17.0已经移除的生命周期函数 生命周期 (1).png

生命周期说明

image.png

/* eslint-disable no-script-url */
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { Component } from "react";

class LifeCycle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: Math.random() * 100,
    };
    this.childRef = React.createRef();
    console.log('parent constructor')
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("parent getDerivedStateFromProps");

    if (nextProps.isUpdate) {
      return { str: "getDerivedStateFromProps update state" };
    }

    return null;
  }

  // componentWillReceiveProps(nextProps, prevState) {
  //   debugger
  //     console.log("componentWillReceiveProps()");
  // }

  componentDidMount() {
    console.log("parent componentDidMount");
    // this.setState({
    //   str: "str",
    // });
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("parent shouldComponentUpdate");
    return true; // 记得要返回true
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("parent getSnapshotBeforeUpdate");
    return {
      name: "componentWillUpdate",
    };
  }

  // componentWillUpdate(prevProps, prevState) {
  //   console.log("componentWillUpdate");
  //   return {
  //     name: 'componentWillUpdate'
  //   }
  // }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("parent componentDidUpdate");
  }

  componentWillUnmount() {
    console.log("parent componentWillUnmount");
  }

  propsChange() {
    console.info("更新父组件state");
    this.setState({
      num: Math.random() * 100,
    });
  }

  setLifeCycleState() {
    console.info("更新子组件state");
    this.childRef.current.setTheState();
  }

  forceLifeCycleUpdate() {
    console.info("强制更新子组件");
    this.childRef.current.forceItUpdate();
  }

  parentForceUpdate() {
    console.info("强制更新父组件");
    this.forceUpdate();
  }

  render() {
    console.log("parent render")

    return (
      <div>
        <button
          className="weui_btn weui_btn_primary"
          onClick={this.propsChange.bind(this)}
        >
          更新父组件state
        </button>
        

        <button
          className="weui_btn weui_btn_primary"
          onClick={this.setLifeCycleState.bind(this)}
        >
          更新子组件state
        </button>
        

        <button
          className="weui_btn weui_btn_primary"
          onClick={this.forceLifeCycleUpdate.bind(this)}
        >
          forceUpdate 子组件
        </button>
        

        <button
          className="weui_btn weui_btn_primary"
          onClick={this.parentForceUpdate.bind(this)}
        >
          forceUpdate 父组件
        </button>
        <Message ref={this.childRef} num={this.state.num}></Message>
      </div>
    );
  }
}

class Message extends Component {
  constructor(props) {
    super(props);
    console.log("child constructor");
    this.state = { str: "hello", name: "rodchen" };
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log("child getDerivedStateFromProps");

    if (nextProps.isUpdate) {
      return { str: "getDerivedStateFromProps update state" };
    }

    return null;
  }

  // componentWillReceiveProps(nextProps, prevState) {
  //   debugger
  //     console.log("componentWillReceiveProps()");
  // }

  componentDidMount() {
    console.log("child componentDidMount");
    // this.setState({
    //   str: "str",
    // });
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log("child shouldComponentUpdate");
    return true; // 记得要返回true
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("child getSnapshotBeforeUpdate");
    return {
      name: "componentWillUpdate",
    };
  }

  // componentWillUpdate(prevProps, prevState) {
  //   console.log("componentWillUpdate");
  //   return {
  //     name: 'componentWillUpdate'
  //   }
  // }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("child componentDidUpdate");
  }

  componentWillUnmount() {
    console.log("child componentWillUnmount");
  }

  setTheState() {
    let s = "hello";
    if (this.state.str === s) {
      s = "HELLO";
    }
    this.setState({
      str: s,
    });
  }

  forceItUpdate() {
    this.forceUpdate();
  }

  render() {
    console.log("child render");
    return (
      <div>
        <span>
          Props:<h2>{this.props.num}</h2>
        </span>
        

        <span>
          State:<h2>{this.state.str}</h2>
        </span>
      </div>
    );
  }
}

export default LifeCycle;

monting阶段

image.png

Contructor

import React from "react";

class LifeCycle extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      name: 'rodchen'
    }
  }
  
  render() {
    return (
      <div>
        <h2>Life Cycle</h2>
        <div>
          
        </div>
      </div>
    );
  }
}

export default LifeCycle;

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

通常,在 React 中,构造函数仅用于以下两种情况:

  • 通过给 this.state 赋值对象来初始化内部 state。
  • 为事件处理函数绑定实例

在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state:

避免将 props 的值复制给 state

constructor(props) {
 super(props);
 // 不要这样做
 this.state = { color: props.color };
}

当props更新不会更新到state。只有在你刻意忽略 prop 更新的情况下使用。此时,应将 prop 重命名为 initialColor 或 defaultColor。必要时,你可以修改它的 key,以强制“重置”其内部 state。

getDerivedStateFromProps

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

  static getDerivedStateFromProps(nextProps, prevState) {
      console.log("getDerivedStateFromProps");
      return {str: "getDerivedStateFromProps update state"};
  }

此方法无权访问组件实例。如果你需要,可以通过提取组件 props 的纯函数及 class 之外的状态,在getDerivedStateFromProps()和其他 class 方法之间重用代码。这里使用this是undefined。

请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。

getDerivedStateFromProps

image.png

UNSAFE_componentWillReceiveProps

componentWillReceiveProps 可以访问组件实例,this是当前的组件

image.png 因为不管什么原因,每次渲染都会调用这个方法,所以默认返回null。如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾​


  static getDerivedStateFromProps(nextProps, prevState) {
      console.log("getDerivedStateFromProps");

      if (nextProps.isUpdate) {
        return {str: "getDerivedStateFromProps update state"};
      }

      return null;
  }

  render() {
      // console.log("render");
      return (
          <div>
              <span>Props:<h2>{this.props.num}</h2></span>
              <br/>
              <span>State:<h2>{this.state.str}</h2></span>
          </div>
      );
  }

render

render() 方法是 class 组件中唯一必须实现的方法。

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

  • React 元素。通常通过 JSX 创建。例如,
    会被 React 渲染为 DOM 节点, 会被 React 渲染为自定义组件,无论是
    还是 均为 React 元素。
  • 数组或 fragments。 使得 render 方法可以返回多个元素。欲了解更多详细信息,请参阅 fragments 文档。
  • Portals。可以渲染子节点到不同的 DOM 子树中。欲了解更多详细信息,请参阅有关 portals 的文档
  • 字符串或数值类型。它们在 DOM 中会被渲染为文本节点
  • 布尔类型或 null。什么都不渲染。(主要用于支持返回 test && 的模式,其中 test 为布尔类型。)

render() 函数应该为纯函数

这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。

如需与浏览器进行交互,请在 componentDidMount() 或其他生命周期方法中执行你的操作。保持 render() 为纯函数,可以使组件更容易思考。

如果 shouldComponentUpdate() 返回 false,则不会调用 render()。

  shouldComponentUpdate() {
      console.log("shouldComponentUpdate");
      return true;        // 记得要返回true
  }

image.png

  shouldComponentUpdate() {
      console.log("shouldComponentUpdate");
      return false;        // 记得要返回true
  }

image.png

componentDidMount

componentDidMount() {
      console.log("componentDidMount");
  }

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

这个方法是比较适合添加订阅的地方。如果添加了订阅,请不要忘记在 componentWillUnmount() 里取消订阅

image.png

你可以在 componentDidMount() 里直接调用 setState()。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。

  componentDidMount() {
      console.log("componentDidMount");
      this.setState({
        str: 'str'
      })
  }

image.png

为什么componentDidMount的执行在父节点之前?

  • 常识理解:父子节点挂在dom节点,如果子节点没有挂载成功,那父组件是不能够表明自己已经挂载成功。
  • 流程说明:Diff算法,render阶段的时候,每个节点的completeWork方法中,会进行当前effectTag节点的标识和建立链表结构effectList。completeWork的节点也是从子节点向上依次执行的,所以在render阶段成功之后,根节点上会具有一条所有标识effectTag需要更新的链表。firstEffectTag指向第一个需要更新的叶子节点。这里做这个链表是为了优化,不能render阶段进行diff算法,到了commit还需要再次进行diff算法。

Updating

更新例子

手动更新父组件state

image.png

手动更新子组件state

image.png

强制更新父组件state

image.png

强制更新子组件state

image.png

shouldComponentUpdate

  shouldComponentUpdate(nextProps, nextState) {
      console.log("shouldComponentUpdate");
      return true;        // 记得要返回true
  }

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()。PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

如果你一定要手动编写此函数,可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,并返回 false 以告知 React 可以跳过更新。请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。

我们不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能。

首次渲染或使用 forceUpdate() 时不会调用该方法

初始加载:

image.png

forceUpdate:

image.png

返回false

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate(),render() 和 componentDidUpdate()

image.png

  shouldComponentUpdate(nextProps, nextState) {
      console.log("shouldComponentUpdate");
      return false;        // 记得要返回true
  }

image.png

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

这个新更新代替componentWillUpdate。 常见的 componentWillUpdate 的用例是在组件更新前,读取当前某个 DOM 元素的状态,并在 componentDidUpdate 中进行相应的处理。


  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    return {
      name: 'componentWillUpdate'
    }
  }

  // componentWillUpdate(prevProps, prevState) {
  //   console.log("componentWillUpdate");
  //   return {
  //     name: 'componentWillUpdate'
  //   }
  // }

  componentDidUpdate(prevProps, prevState, snapshot) {
      console.log("componentDidUpdate");
      debugger
      console.log(snapshot)
  }

image.png

componentWillUpdate & getSnapshotBeforeUpdate的区别

www.jianshu.com/p/b91e95af2…

  • 在 React 开启异步渲染模式后,在 render 阶段读取到的 DOM 元素状态并不总是和 commit 阶段相同,这就导致在componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
  • getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的。
  • 此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。你也可以在 componentDidUpdate() 中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。

componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

getSnapshotBeforeUpdate & componentDidUpdate的顺序

看上面手动更新父组件state,产生的更新。生命周期执行的顺序如下。这里getSnapshotBeforeUpdate和componentDidUpdate的执行顺序是分开的,同样都是从子组件开始,然后到父组件。 image.png

这里要从commit的三个阶段来说:commit分为三个阶段

  • before mutation 阶段:操作DOM之前【getSnapshotBeforeUpdate】
  • mutation 阶段:操作DOM【componentWillUnmount】
  • layout 阶段:操作DOM之后【componentDidMount,componentDidUpdate】

和上面为什么componentDidMount的执行在父节点之前?说的一样,commit阶段主要是在遍历在render阶段形成effectList。因为effectList的顺序是从叶子节点开始的。所以这里的顺序是从子节点到父节点。那么为什么分为两个批次。因为这两个方法是在commit的不同阶段执行的。

Unmounting

componentWillUnmount

componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

卸载组件​

image.png

这里的原因是当父节点在进行diff算法的时候,标识当前节点需要删除,则会结束当前节点的继续遍历。而在commt的mutation阶段,进行delete case的时候,会对父节点进行遍历所有子节点,移除进行delete操作。

性能优化

React.PureComponent

React.PureComponent 与 React.Component 很相似。两者的区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。 ​

如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

React.memo

React.memo 为高阶组件。 ​

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。 ​

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。 ​

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

参与评论讨论有奖

  1. 生命周期的组件的问题讨论
  2. 关于react hooks的问题探讨