React 生命周期详解

生命周期是一个极其重要的概念,特别是在软件开发领域更是起着举足轻重的作用。在 React 中,生命周期大致上可以分为初始化(Initialization)、挂载(Mounting)、更新(Updating)和卸载(Unmounting) 这几个阶段,每个阶段又会分别调用不同的生命周期函数。这些函数的定义在 React 的最近几个更新的版本中又得到了不同程度的更新。接下来,我们就根据从较久版本到新版本的顺序来捋一捋!

React v16.0 之前

这一个版本的生命周期应该算是大家最为熟知的一套了,因为我们一般刚开始接触 React 开发的时候,就是使用的这套生命周期。这套起初的生命周期算是分得很清晰的了,有四个阶段。

初始化(Initialization)

在这个阶段主要进行相关数据的初始化和装载,包括 props 和 state。在 ES5 里面指的是 getDefaultProps() 和 getInitialState() 两个函数的调用,而在 ES6 中就变成了如下的形式(constructor):

import React, { Component } from 'react';

class Test extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }
}
复制代码

首先定义了一个名为 Test 的类,也就是组件,该类继承自 React Component 类。随后定义了一个属于自己的构造函数,该构造函数会接受一些父组件传来的参数。在构造函数中通过 super() 调用了父类(即 React Component)的构造函数进行了初始化,以后就可以在 Test 类中使用 render 和生命周期函数了。最后对 this.state 进行了初始化。

挂载(Mounting)

在组件的挂载阶段会涉及到 componentWillMount、render 和 componentDidMount 这个三个函数:

  • componentWillMount:该生命周期函数在组件挂载到 DOM 之前进行调用,在 React v16.0 之前的版本只会调用一次(在 Fiber 架构中可能会多次调用)。在这个函数里面,你可以任意调用 setState 修改状态,而且不会导致组件重复渲染。当然一种推荐并且优雅的写法是提到 constructor 构造函数里面,所以慢慢的这个函数会很少被用到。

⚠️注意:有人可能会在这个函数里面获取 API 接口的数据以便第一次渲染的时候就带上数据,但事实上并不能实现这个设想,因为 AJAX 是一个异步的 IO 操作,而整个生命周期过程是一个同步【在 Fiber 架构中加入了 async render】的过程,而且在 SSR 模式中,该函数也会在服务端进行调用,像这样的 IO 操作最好是放在 componentDidMount 执行。在 Fiber 架构中启用 async render 之后,更不应该在 componentWillMount 里做 AJAX 这样的操作了,因为componentWillMount 可能会被调用多次,谁也不会希望无用的 AJAX 被调用吧;

  • render:该函数并不会像它的名字一样负责实际的渲染工作,而是会根据 props 和 state 的数据状态返回新的 React UI 组件,随后由 React 内部的机制去恰当的渲染页面 DOM。同时 render 函数必须保证是一个纯函数,它的输出只能依赖 props 和 state 的数据状态。

⚠️注意:不能在这个函数中执行 setState 修改数据状态,否则会引起副作用,导致组件渲染进入死循环;

  • componentDidMount:在组件完成挂载到 DOM 后调用,在整个组件生命周期中只会被调用一次。于是,在这个函数中获取 API 接口数据是最最合适不过的。

更新(Updating)

这个阶段是整个生命周期中最复杂的阶段,也是广大开发者最为关注的阶段,因为这个阶段的渲染性能直接影响到用户的体验和项目的呈现效果。

首先分析一下什么情况下会进入这个阶段,然后说说如何利用有效的办法避免没必要的重新渲染:

父组件的重新渲染

因为 props 是一个对象,每当父组件重渲染时,都会将新的 props 对象传递给子组件,导致子组件进行更新阶段(无论 props 有无变化),最终引发重渲染。当然这些重渲染大多数都是不必要的,我们可以利用 shouldComponentUpdate 函数的特性(效果类似于 PureComponent,但是比 PureComponent 定制化更强)进行避免:

class Child extends Component {
  shouldComponentUpdate(nextProps){ 
    // 通过比较 props 里面的一些值来决定该函数是返回 false 还是 true
    // 已决定后面的渲染操作时候继续
    if(nextProps.someThings === this.props.someThings){
      return false
    }
    return true;
  }
  
  render() {
    return <div>{this.props.someThings}</div>
  }
}
复制代码

组件本身调用 setState

当组件自身调用 setState 时,无论 state 值有无变化,都会导致组件重新渲染。我们同样可以利用shouldComponentUpdate 方法的特性来优化(效果类似于 PureComponent,但是比 PureComponent 定制化更强)。

class Child extends Component {
  constructor(props) {
    super(props);
    this.state = {
      someThings: 1
    }
  }
  
  // 这里会判断 nextStates 和 this.state 里面的某值是否有变化
  // 有变化就返回 true,继续执行渲染逻辑
  // 没有变化就返回 false,停止执行渲染逻辑
  shouldComponentUpdate(nextStates){
    if(nextStates.someThings === this.state.someThings){
      return false
    }
    return true;
  }
  
  handleClick = () => { 
    // 虽然调用了setState ,但state并无变化
    const preSomeThings = this.state.someThings
    this.setState({
      someThings: preSomeThings
    })
  }
  
  render() {
    return <div onClick = {this.handleClick}>{this.state.someThings}</div>
  }
}
复制代码

这里就先介绍这两种避免重复渲染的方法,其实避免没必要的渲染的方法还有很多,以后我会单独开一篇进行讲解。

处理逻辑详解

由于 React 组件重渲染的机制是根据 props 和 state 这两个状态值来处理的,所以会出现出现三种不同情况的渲染过程:

  • 1、只有父组件传过来的 props 变化引发的更新依次会涉及到 componentWillReceiveProps、shouldComponentUpdate**(如果这个函数返回 false,那么后续的函数就不会执行)**、componentWillUpdate、render 和 componentDidUpdate 这五个函数的调用;

  • 2、只有组件自身的 state 变化引发的更新依次会涉及到 shouldComponentUpdate**(如果这个函数返回 false,那么后续的函数就不会执行)**、componentWillUpdate、render 和 componentDidUpdate 这四个函数的调用;

  • 3、当父组件传过来的 props 和组件自身的 state 变化引发的更新就会依次涉及到 componentWillReceiveProps、shouldComponentUpdate**(如果这个函数返回 false,那么后续的函数就不会执行)**、componentWillUpdate、render 和 componentDidUpdate 这五个函数的调用。

接下来我们就来详细看看这几个函数到底做了什么!

  • componentWillReceiveProps(nextProps):当父组件传递的 props 即将引起组件更新时会被调用,该方法接受一个参数指的是当前父组件传递给组件的最新的 props 状态数据。在这个生命周期方法中,我们可以根据比较 nextProps 和 this.props 新旧 props 的值查明 props 是否改变,依次做一些数据处理的逻辑。

⚠️注意:当父组件重新 render 时,并不能保证 nextProps 是有变化的。

  • shouldComponentUpdate(nextProps, nextState):这个生命周期函数是 rerender 前必须调用的,不管是 props 引起的重渲染还是 setState 引起的重渲染。这个函数有一个极其重要的特性:它需要返回true 或者 false 来标识组件更新渲染是否继续,如果是返回 true 就继续重渲染逻辑,如果是返回 false 就停止重渲染逻辑。我们可以根据该生命周期函数的这一特性通过比较 nextProps 和 this.props、nextState 和 this.state 减少组件不必要的渲染,优化组件性能。

⚠️注意:如果在此声明周期函数调用之前执行了 setState ,这个函数获取 this.state 的值并不会是最新的,而是将新的 state 值传递给 nextState。如果 this.state 的值也是新值,那么 this.state 和 nextState 比较的结果就会永远是 true 了,毫无意义!

  • componentWillUpdate(nextProps, nextState):当 shouldComponentUpdate 函数返回 true 时,接下来就会执行该声明周期方法,可以执行一些组件更新之前的一些操作,一般用得比较少。

  • render:调用时机和作用同上面挂载(Mounting)阶段的 render 介绍,这里不再赘述。

  • componentDidUpdate(prevProps, prevState):此生命周期方法在组件更新完后被调用。因为组件已经重新渲染了,这里可以对组件当前的最新的 DOM 元素进行操作。该方法接受 prevProps 和 prevState 这两个参数,分别指的是组件更新前的 props 和 state。

卸载(Unmounting)

当页面跳转或者组件卸载之前会到这个生命周期阶段,该阶段值涉及到 componentWillUnmount 一个方法,并且在整个组件生命周期中只会被调用一次。我们可以在这个方法执行清除定时器、解绑事件等等一些清理工作,方式内存泄漏。

React v16.3

React v16 推出了 Fiber 架构,当开启 async rendering 后,React v16 之前的生命周期函数中的 render 函数之前执行的函数都有可能会执行多次,再加上原有的生命周期函数总是会诱惑开发者在 render 之前的生命周期函数中做一些异步或者影响性能的动作,现在这些动作如果还是放在这些函数中的话,有可能会被调用多次,这肯定不是你想要的结果。

getDerivedStateFromProps

随后在 React v16.3 中引入了 getDerivedStateFromProps 静态生命周期函数,deprecate 了一组生命周期 API,这里面包括 componentWillMount、componentWillReceiveProps 和 componentWillUpdate。大家可以拿这个生命周期图和 v16 之前的生命周期图进行对比。会发现 getDerivedStateFromProps 的推出,将除 shouldComponentUpdate 之外的 render 之前的所有生命周期函数全干掉了,真的是大块人心,避免了生命周期方法滥用的情况。

// 由于 getDerivedStateFromProps 是一个静态函数,所以不能使用 this
// 导致其完全是一个纯函数,只能做简单的运算,这样就够了
static getDerivedStateFromProps(nextProps, prevState) {
  //根据 nextProps 和 prevState 计算出预期的状态改变,返回结果会被送给 setState
}
复制代码

每当父组件引发当前组件的渲染时,getDerivedStateFromProps 会被调用,这样我们有机会可以根据新的 props 和之前的 state 来调整新的 state。如果放在三个被 deprecate 生命周期函数中实现比较纯,没有副作用的话,就可以搬到 getDerivedStateFromProps 了;如果不幸做了类似 AJAX 之类的操作,首先要反省为什么自己当初这么做,然后搬到 componentDidMount 或者 componentDidUpdate 中去。目前当你使用三个被 deprecate 生命周期函数时,开发模式下会有红色警告,要求你使用 UNSAFE_ 前缀。可能会在打一次大版本更新时直接废弃,所以那些抱有侥幸心理的开发者还是放弃使用吧。

⚠️注意:当你同时使用了 getDerivedStateFromProps、 getSnapshotBeforeUpdate 新的生命周期 API 和 deprecate 生命周期函数时,deprecate 生命周期函数会被直接忽略掉,并不会适时执行!

getSnapshotBeforeUpdate

React v16.3 还引入了一个新的生命周期函数 getSnapshotBeforeUpdate,这函数会在 render 函数调用之后执行,而执行的时候 DOM 元素还没有被更新。在这个函数中我们可以获取 DOM 信息和操作 DOM 元素,随后计算出一个 snapshot。这个 snapshot 会作为 componentDidUpdate 的第三个参数传入。

getSnapshotBeforeUpdate(prevProps, prevState) {
  console.log('#enter getSnapshotBeforeUpdate');
  return 'lane';
}

componentDidUpdate(prevProps, prevState, snapshot) {
  // 这里 snapshot 值是 'lane'
  console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
复制代码

咋一看还以为 snapshot 是组件级别的“快照”,但是其实它可以是任何值。那到底怎么用,完全是看开发者自己了。getSnapshotBeforeUpdate 把 snapshot 返回,然后 DOM 改变将 snapshot 传递给 componentDidUpdate。官方文档给出了一个处理 scroll 的例子,大家可以参考使用,但是大部分情况下都用不到。

问题

大家在仔细看看这个版本的生命周期图,发现 getDerivedStateFromProps 只有会在父组件引发的 Updating 过程中才会被调用。如果是因为自身 setState 或者 forceUpdate 引发,而不是不由父组件引发的更新,那么getDerivedStateFromProps 是不会被调用的。关于这些个差异化,无论是项目的开发者还是项目的维护者理解起来都很费劲,而且由组件自身 setState 或者 forceUpdate 引发的更新过程莫名其妙会少了再次修改状态的操作。

还好,React 官方很快意识到了这个问题。于是在 React v16.4 中修改了这一点,修改的结果就是让getDerivedStateFromProps 无论是在 Mounting 阶段还是在 Updating 阶段,也无论是因为什么引起的 Updating 全部都会被调用。于是就有了 React v16.4 版本的生命周期图谱!

React v16.4

这个版本的生命周期就不重复介绍了,大家对比图谱就可以发现区别了。React v16.4 生命周期改动就是:无论是在 Mounting 阶段还是在 Updating 阶段,也无论是因为什么引起的 Updating 全部都会触发 getDerivedStateFromProps 的调用。这样之前说的那些问题就不存在了,理解起来也容易了很多。

延伸

其实在 React v16.0 刚推出的时候,增加了一个 componentDidCatch 生命周期函数,这个修改只是增量式的,完全不影响原有生命周期函数。这个生命周期函数的作用是:如果 render() 函数抛出错误,则会触发该函数捕获异常,而不会因为一个组件的异常导致整个应用直接崩掉。

// 使用示例
class PotentialError extends React.Component {   
  constructor(props) {     
    super(props);     
    this.state = { error: false };
  }
  
  // 会捕获组件的报错,并包含堆栈信息
  componentDidCatch(error, info) {     
    this.setState({ error, info });
  }
  render() {
    if (this.state.error) {
      return <h1>Error: {this.state.error.toString()}</h1>;
    }
    return this.props.children;   
  } 
}
复制代码

⚠️注意:错误在渲染阶段中被捕获,但在事件处理程序中不会被捕获

总结

详细了从较旧版本到新版本的生命周期变化,以及其中涉及到的 API 函数,我个人算是对 React 生命周期有了一个全新的认识,算是一个不错的总结了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值