React16 生命周期函数深刻浅出

本篇博文基于 React 16.5.2javascript

吐槽:

做为一个后端开发,15年开始关注大前端发展趋势,于17年去线下听了场前端开发会议,那个时候Vue2.0刚出没多久,就被那快速构建页面给吸引了。最先重返前端仍是大半年前,新项目用vue写了几个功能页面,发现如今写前端是真挺舒服,尤为是对于后端人员来讲(排除掉CSS),快速入门并上手不是什么问题。html

至于为何最终选择了react而非vue?是由于当时对react和vue及RN和weex作了番调研对比,介于weex的不给力,及后期react和vue学习成本差很少,但react的社区更为活跃,外加发起者背景,就毅然选择了react。(我我的是通一精百的支持者,因此对于react的理念(learn once,write anywhere),是很同意的。而weex的理念(write once,run anywhere)虽然很吸引人,但时下我的以为技术并未达到此程度,配置的复杂度及大量的轮子需造,难以知足大型项目的要求。包括目前JD推出的Taro,我的目前持观望态度,等到react这块应用到项目以后,再码一波Taro实际调研一番)。前端

接触React的时候已是React 16.3,不由感慨前端发展至今,越有后端的趋势。先后花了3个多月的时间过了一遍webpack4,npm,react,在公司内部作了几场培训,发现了其中的一些不协调,但随着版本的迭代,这些不协调也依次在被更正。(看React17的更新内容,本来一些摸棱两可的方法、属性元素命名均会获得改善:)vue

后续会以此文为契机,开一个专栏,记录、分享公司现有电商项目逐步迁移至react技术栈。java

正文

本篇博文会对React16.5.2中的经常使用生命周期函数作一些翻译、讲解说明及发表一些我的的理解和见解,若有错误、歧义之处,还望你们不吝啬指出,互相交流:)react

开篇借用官方提供的生命周期函数图:(虽然是React 16.4的,但一样适用于16.5版本) webpack

http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
出处: projects.wojtekmaj.pl/react-lifec…

react组件生命周期函数说明官方地址:reactjs.org/docs/react-…web

getDerivedStateFromProps()方法的16.3版本与以后的16.4和16.5有所调整:npm

  • 在16.3版本上,该方法仅在props变更时才会被触发。canvas

  • 在16.3以后的版本上,该方法调整为props和state的变更都会触发该方法。

从上图中,咱们能够看到,React的生命周期函数主要集中在三个阶段:挂载阶段(Mounting),更新阶段(Updating)和卸载阶段(Unmounting)。

其中:

  • Mounting阶段主要有这几个方法须要注意:
    • constructor(props)
    • componentWillReceiveProps(nextProps)
    • getDerivedStateFromProps(props, state)
    • componentWillMount()
    • render()
    • componentDidMount()
  • Updating阶段主要有这几个方法须要注意:
    • componentWillReceiveProps(nextProps)
    • getDerivedStateFromProps(props, state)
    • shouldComponentUpdate()
    • componentWillUpdate()
    • render()
    • componentDidUpdate()
  • Unmounting阶段只有一个方法须要注意:
    • componentWillUnmount()
  • 额外的几个方法说明:
    • componentDidCatch()
    • this.setState()
    • this.forceUpdate()

下面咱们就对以上生命周期函数作进一步说明:

Mounting:

constructor(props)

  • 在构造函数中,通常就作两件事情:
    1. 初始化state
      • 在构造函数中初始化state需直接赋值,不能调用 this.setState() 方法。
      • constructor()是惟一可以让你直接给 this.state 赋值的地方,可能有同窗会说能够在构造函数外(跟constructor同级)直接给 state 赋值,确实,由于 state 派生自 Component 或者 PureComponent 模块,在外层直接赋值,实际上是在给基类中的 state 赋值,而在 constructor() 使用 this.state = {...} 进行初始化,实际指的是当前组件这个实例。如下两种赋值方法都可:
        class TestContainer extends PureComponent {
        
            state = {
                content:"1"     //先执行
            }
        
            constructor(props){
                super(props);
                this.state={
                    content:"2" //后执行,最终 this.state.content 为 2
                }
            }
            。。。
        }
        复制代码
    2. 绑定方法:this.handleClick = this.handleClick.bind(this);
      • 我所认为的是:在构造函数中进行方法的绑定后,在使用的时候能够减小匿名方法的产生,进而提升性能。
  • 若是不须要初始化state,或者绑定方法,那么能够不用实现constructor(props)。
  • 若是实现了这个方法,那么应当在方法体的最开始调用 super(props),不然会致使 this.props 为 undefined 的问题。
  • 不该在该方法中进行"事件的订阅/引入有side-effects的方法"。(这种应该放在 componentDidMount() 中)

这里的 side-effects 方法,翻译出来是 "反作用" 的意思,我的以为不妥,有点生涩,可能翻译成"附加做用"更为稳当。

它指的是,render()方法应该只完成它的主要功能(如初始化state和绑定方法),不该顺带完成其余的附加功能,如统计,动画等。

componentWillReceiveProps(nextProps)

该方法的应用场景:根据特定的props变换,来触发state的更新。如咱们可能会有一个canvas用来表示页面loading百分比,这个时候就能够根据传入的nextProps中的百分比属性跟当前state的百分比属性作对比,若不一致,则更新,以下:

componentWillReceiveProps(nextProps) {
        if (this.props.percent !== nextProps.percent) {
            this.setUpCircle(nextProps.percent);
        }
    }
复制代码
  • 该方法在React16版本中已经被标记为unsafe,虽然目前依然可使用(即便用 componentWillReceiveProps 或者 UNSAFE_componentWillReceiveProps 都可),但在 React17 中将会移除,再也不建议使用【该方法将会被 getDerivedStateFromProps() 取代。】
  • 在组件对新(下一个) props 执行任何操做以前,该方法会被触发,并将 下一个props 做为参数。
  • 初始渲染时该方法不会被触发。
  • 能够在该方法内调用 this.setState(),但并不建议调用,由于这会致使只要组件更新,这个方法就会被执行。

getDerivedStateFromProps(props, state)

该方法的应用场景:使组件可以根据父组件传入的 props 来更新其自身的 state

在16.4版本以前,只有props更新才会触发该方法;但FB在16.4版本作了完善,目前 props 和 state 的更新均会触发该方法。

  • 该方法的两个入参,分别表示:
    • props:父组件传入的值。可能重命名为 nextProps 更为直观。
    • state:组件自身的state,至关于 this.state,可能重命名为 prevState 更好理解。
  • 该方法在调用 render() 方法前被触发。
  • 若是父组件的state进行了更新,那么其子组件也会触发 getDerivedStateFromProps() 方法。
  • 使用该方法的时候须要初始化 state,不然在控制台中会出现警告信息,以下图:
  • 使用该方法,须要在该方法中返回一个:对象/null:
    • 若是返回的是对象,则会更新 state。
    • 若是返回的是null,则表示不更新。
  • 该方法用于取代:componentWillReceiveProps(nextProps) 生命周期函数。
    • getDerivedStateFromProps(props, state) 和 componentWillReceiveProps(nextProps) 的差别:
      • componentWillReceiveProps(props, state):仅在父组件更新时触发。
      • getDerivedStateFromProps(props, state):除了父组件更新,其自身的state更新时也会触发。
  • 该方法没法访问组件的实例,换句话说,不能在该方法内部,调用 this.state,this.myFunction() 等实例对象/方法。

componentWillMount()

使用场景:能够在根组件的componentWillMount中作一些App的配置。

  • 该方法在React16版本中已经被标记为unsafe,虽然目前依然可使用(即便用 componentWillMount 或者 UNSAFE_componentWillMount 都可),但在 React17 中将会移除,再也不建议使用
  • 该方法将会被 getDerivedStateFromProps() 取代。
  • 该方法是SSR(server-side render)渲染上惟一的生命周期函数。
  • 不要在该方法中使用 rpc 请求加载组件的数据。
  • 不能在该方法中调用 this.setState()。

render()

  • 组件中惟一必须的方法。

  • render()方法的返回类型目前有 string,number,boolean,null,portal 以及 fragments(带有key属性的数组),eg:

    render() {
            return "string";    //string
            return 123;         //number
            return true;        //若是返回的是false,则什么都不渲染
            return null;        //若是返回的是null,则什么都不渲染
            return ReactDOM.createPortal(<MyComponent></MyComponent>, domNode);//portal
            return [            
                <div key="unique_1">若是返回的是数组类型</div>,
                <span key="unique_2">须要在每一个html元素上加上 key 值</span>,     //fragments
                <p key="unique_3">不然控制台会报错</p>
            ]
        }
    复制代码
  • 须要注意的是,组件在更新的时候,也会触发 render() 方法,但若是 shouldComponentUpdate() 返回的是false,则在"更新阶段",render()方法不会被触发。

  • 该方法应为一个纯函数,除了作渲染的动做外,不该顺带完成其余动做(如 setState 等),即应避免 side-effects。

  • 应尽可能仅在 render() 这个生命周期函数中中从 this.props 和 this.state 中读取数据。

componentDidMount()

使用场景:总体来讲就是作“获取一些数据”,或者作必需要有 DOM 才能作的设置。

  • 只会触发一次:在组件被挂载到页面以后被触发,以后就不会再执行了(如页面的更新等都不会再触发,需注意)。
  • 通常一些 rpc 请求会放到这个方法中进行调用。
  • 通常事件的订阅会写在该方法中(可是不要忘记在组件unmount的时候进行取消订阅。)
  • 一些须要初始化DOM节点的操做能够放在这个声明周期函数中进行。
  • 能够在该方法中使用 this.setState()。但须要注意,该操做会在浏览器更新屏幕以前发生,对于用户而言是无感的(即在该方法中又额外调用了一次this.setState,致使render()触发了2次,但用户的感觉依然是屏幕更新了一次)。不过要谨慎在该方法中调用 this.setState,会致使性能问题。

Mounting 阶段,生命周期函数执行顺序以下图(包含子组件)

排除了 UNSAFE 的方法。

Updating:

componentWillReceiveProps(nextProps)

getDerivedStateFromProps(props, state)

shouldComponentUpdate(nextProps, nextState)

使用场景:通常用于精确控制组件是否须要从新渲染(这个方法通常用于性能优化),在绝大多数状况下,每次 state 的更改都应该让组件从新渲染。

  • 只要有一个字段进行了更新,那么其余全部字段都会进行更新,这个会减慢页面速度。(经过shouldComponentUpdate,容许咱们只有当咱们关心的 props 更新的时候,才进行组件的更新)。
  • 但须要谨慎使用,由于一旦你本身忘记实现了这个方法,可能会致使你的react组件没法正常更新。
  • 该方法返回的数据类型是一个boolean,用于决定组件是否应该更新。
    • 默认返回 true。
    • 当返回 false 的时候,阻止的是当前的组件进行更新,对于当前组件的"子组件",并不受影响。
  • 不该该在继承于 PureComponent 的组件中显示实现 shouldComponentUpdate(),控制台会有警告。
  • 目前,当该方法返回false的时候,componentWillUpdate()、更新时的 render() 以及 componentDidUpdate() 都将不会被触发。(FB在官网上说后期有可能会调整该方法:即便返回 false,依然有可能会触发再次渲染,需留个心)
  • 何时该方法不会被触发?
    • 组件第一次渲染时。
    • 在组件中调用 this.forceUpdate() 时。
  • 我的建议:
    • 该方法应尽可能不去手动实现。
    • 不该使用该方法来作一些防止组件渲染的操做,这可能会致使一些莫名的bug,如组件明明应该更新,可是却没有更新。
    • 优先能够考虑继承 PureComponent 而非 Component 来优化组件而不是手动实现shouldComponentUpdate。
  • 不能在这个方法中使用 this.setState()。

componentWillUpdate(nextProps, nextState)

使用场景:当你实现了 shouldComponentUpdate (返回 true时) 而且在 props 更改时须要执行某些操做的时候,那么 componentWillUpdate 这个方法仍是有点意义的,但我的认为,做用不是太大,反却是增长了理解的复杂度,被删除也是情理之中。

  • 该方法在React16版本中已经被标记为unsafe,虽然目前依然可使用(即便用 componentWillUpdate 或者 UNSAFE_componentWillUpdate() 都可),但在 React17 中将会移除,再也不建议使用
  • 从功能上来讲,componentWillUpdate(nextProps, nextState) 跟 componentWillReceiveProps(nextProps) 基本相同,只是前者不容许调用 this.setState()。可是,这2个方法
  • 当 shouldComponentUpdate 返回 false时, 该生命周期函数将不会被触发。
  • 不能使用setState。

render()

componentDidUpdate(prevProps, prevState, snapshot)

使用场景:

  • 通常在这个方法中进行 RPC 请求。(能够比较下先前的 props 和当前的 props 是否一致,若一致,则能够不用请求网络数据。)
  • 若是想要在DOM自身更新以后作一些动做(如从新排列网格等),那么能够在这个方法中进行。
  • 该生命周期函数在组件更新完成后当即执行。组件第一次渲染的时候,该生命周期函数不会被触发。
  • 能够作跟 componentDidMount 中所做的相同的事情:如重置布局,重绘画布等
  • 可使用setState。

Updating 阶段,生命周期函数执行顺序以下图(包含子组件)

排除了 UNSAFE 的方法。

Unmounting:

componentWillUnmount()

  • 该方法在组件将要被卸载的时候触发。
  • 另外,在渲染期间,当前组件/其子组件的componentDidMount()函数发生错误时,该方法也将被触发。
  • eg:
    • 我刻意在"嵌套在子组件中的组件"中的componentDidMount()方法中,抛出了一个异常,此时该组件往上的父级组件均触发了 componentWillUnmount()
      • 我尝试在construct(),componentDidUpdate()等生命周期函数中抛出异常,均不会触发 componentWillUnmount()。

Unmounting 阶段,生命周期函数执行顺序以下图(包含子组件)

这里须要注意下,该函数是 will unmount,因此触发顺序上是从父组件到子组件,但释放顺序上,是最内层的子组件先释放,最终最外层的根组件才释放。

额外的几个方法说明:

componentDidCatch(error,info)

使用场景:UI中的一些JS错误不该该使整个App崩溃,为了解决这个问题,React16中引入了Error Boundary(错误边界)这个概念,旨在解决容许页面的部分组件异常但不影响App的渲染。能够认为是组件中的 try-catch。而为了实现这个功能,就须要借助 componentDidCatch() 这个生命周期函数。

  • Error Boundaris是自定义的 react 的组件,这些组件能够捕获其子组件的异常,并显示子组件错误时的替代组件内容,而App不会崩溃。
  • 怎么样的组件能够认为是 Error Boundary 组件?
    • 只要该组件内部实现了 componentDidCatch 这个方法就能够认为是 ErrorBoundary 组件,如:

      ...
      class MyErrorBoundary extends Component{
          ...
          componentDidCatch(error,info){
              ...
          }
          ...
      }
      复制代码
  • 一个例子:当子组件发生错误时,显示"sth wrong here."
    • MyErrorBoundary:

      import React, { Component } from 'react';
      class MyErrorBoundary extends Component {
          state = {
              isError: false
          }
          render() {
              if (this.state.isError) {
                  return (<div>sth wrong here.</div>);
              }
              return this.props.children;
          }
          componentDidCatch(error, info) {
              console.log(error, info);
              this.setState({
                  isError = true
              });
              //也能够作其余的一些事情,如日志,异常数统计等
          }
      }
      export default MyErrorBoundary;
      复制代码
    • App.js

      ...
      render(){
          return (
          ...
          <MyErrorBoundary>
              <OtherComponent /> </MyErrorBoundary>
          ...
          );
      }
      ...
      复制代码
  • ErrorBoundary 组件只能捕获其子组件,没法捕获其自身的异常。
  • 为何用 ErrorBoundary 这样的组件?直接在代码中 try-catch 不是很直观?
    • 这个问题,就仁智各见了,try-catch 面向的是 代码,而 ErrorBoundary 面向的是组件:这里我想用一个后端例子来讲明,在写后端代码时(好比一个 Web Api) ,你能够在具体业务代码中使用 try-catch 来对可能出现异常的代码块作处理,但有的时候,你可能想针对全部“方法”作异常处理,这个时候使用 AOP 的方式来写一个异常处理的方法更好(或者能够认为是 Exception filters这样的自定义类)
  • 注意事项:若要查看ErrorBoundary的效果,没法在开发模式下进行(即mode=development)【或者直接运行 npm start,是不会出效果的,会显示具体的错误信息。】,有两种方法能够解决:
      1. 使用 npm run build命令将项目打包后发布到服务器上进行;
      1. 将项目的模式从development调整为 production
      • 需注意,如果使用create-react-app来建立的项目,配置文件须要解包后才能进行编辑,运行命令: npm run eject,解包后调整"/scripts/build.js",将 process.env.NODE_ENV 赋值为 production,以后再运行 npm start 的时候就能看到上面示例代码的效果。

this.setState()

  • 用来设置state的值。

  • 在16.3开始,FB建议使用setState的异步函数写法,如: 原先咱们在使用的时候是直接进行赋值:

    inputHandler = (e) => {
        this.setState({
            inputValue: e.target.value              //再也不这么作
        });
    )
    复制代码

而是改用:

```javascript
    inputHandler = (e) => {
        let value = e.target.value;
        this.setState(() => {
            return {inputValue: value};   //使用方法的形式,最终再返回一个对象,这里须要注意下,这么写是异步的,但存在一个问题,即输入的内容,须要再方法外层先获取到:如这里的value。
        })
    }
    ```
复制代码
  • 异步写法中,该方法提供了一个回调函数,经过该回调函数,能够确保只有等到setState触发完成以后,才会执行回调的方法(另一个能够确保在setState执行完成以后再执行的点是componentDidUpdate方法),如:

    this.setState((prevState, nextState)=>{
        ...    
    }, callback);
    复制代码
    • 该方法提供了2个入参,prevState 和 props,前者至关因而 this.state。

    • 只有当setState方法的第一个参数执行完成以后,才会执行 callback方法。

    • 须要注意的是,由于setState是一个异步方法,因此在赋值的时候须要注意下,若是须要从表单或者其余地方获取值赋值给state的某一个属性,须要先把这个值在setState方法以前赋给一个变量,再在setState方法中使用这个变量。如

      let name = e.target.value;
          this.setState((prevState, props)=>{
              return {
                  userName : name  //不可以直接 userName:e.target.value,异步方法中获取不到当前上下文
              }    
          }, callback);
      复制代码
  • 补充:对于 setState ,其不必定在调用 setState 的时候就当即触发这个动做。为了性能,react会自行判断,将组件的全部setState在同一个时间点一同执行(批量执行),而非调用一次就执行一次。

  • 每一次 setState 都会致使组件的再次渲染,除非 shouldComponentUpdate 返回 false。

this.forceUpdate()

  • 调用forceUpdate()的时候,将会跳过 shouldComponentUpdate()而直接从新render()组件。
  • 父组件中调用 forceUpdate 亦会致使子组件的生命周期函数被触发(包括componentDidUpdate)。
  • 正常状况下,这个方法不多使用。
相关文章
相关标签/搜索