React - 组件的生命周期

336 阅读9分钟

组件的生命周期

React严格定义了组件的生命周期,生命周期可能会经历如下三个过程:

  • 装载过程(Mount),也就是把组件第一次在DOM树中渲染的过程;
  • 更新过程(Update),当组件被重新渲染的过程;
  • 卸载过程(Unmount),组件从DOM中删除的过程。 三种不同的过程,React库会依次调用组件的一些成员函数,这些函数称为生命周期函数。所以,要定制一个React组件,实际上就是定制这些生命周期函数。

装载过程

我们先来看装载过程,当组件第一次被渲染的时候,依次调用的函数是如下这些:

  • constructor
  • getInitialState
  • getDefaultProps
  • componentWillMount
  • render
  • componentDidMount
  1. constructor

我们先来看第一个constructor,也就是ES6中每个类的构造函数,要创造一个组件类的实例,当然会调用对应的构造函数。

要注意,并不是每个组件都需要定义自己的构造函数。在后面的章节我们可以看到,无状态的React组件往往就不需要定义构造函数,一个React组件需要构造函数,往往是为了下面的目的:

  • 初始化state,因为组件生命周期中任何函数都可能要访问state,那么整个生命周期中第一个被调用的构造函数自然是初始化state最理想的地方;
  • 绑定成员函数的this环境

在ES6语法下,类的每个成员函数在执行时的this并不是和类实例自动绑定的。而在构造函数中,this就是当前组件实例,所以,为了方便将来的调用,往往在构造函数中将这个实例的特定函数绑定this为当前实例。

	this.onClick = this.onClick.bind(this);

通过 bind 方法让当前实例中 onClick 函数被调用时,this 始终是指向当前组件实例。

注意

在某些教程中,大家还会看到另一种bind函数的方式,类似下面的语句:

	this.onClick = ::this.onClick;

这里所使用的两个冒号的::操作符叫做bind操作符,虽然有babel插件支持这种写法,但是bind操作符可能不会成为ES标准语法的一部分,所以,虽然这种写法看起来很简洁,我们平时并不使用它。

  1. getInitialState和getDefaultProps

getInitialState这个函数的返回值会用来初始化组件的this.state,但是,这个方法只有用React.createClass方法创造的组件类才会发生作用,本书中我们一直使用的ES6语法,所以这个函数根本不会产生作用。

getDefaultProps函数的返回值可以作为props的初始值,和getInitialState一样,这个函数只在React.createClass方法创造的组件类才会用到。总之,实际上getInitialState和getDefaultProps两个方法在ES6的方法定义的React组件中根本不会用到。

假如我们用React.createClass方法定义一个组件Sample,设定内部状态foo的初始值为字符串bar,同时设定一个叫sampleProp的prop初始值为数字值0,代码如下:

const Sample = React.createClass({
      getInitialState: function() {
        return { foo: 'bar' }
      },
      getDefaultProps: function() {
      	return { sampleProp: 0 }
      }
 })

用ES6的话,在构造函数中通过给this.state赋值完成状态的初始化,通过给类属性(注意是类属性,而不是类的实例对象属性)defaultProps赋值指定props初始值,达到的效果是完全一样的,代码如下:

class Sample extends React.Component {
  constructor(props) {
    super(props);
    this.state = { foo: 'bar' };
  }
}
Sample.defaultProps = {
  return { sampleProp: 0 }
}

React.createClass已经被Facebook官方逐渐废弃,但是在互联网上还能搜索到很多使用React.createClass的教材,虽然强烈建议不再要使用React.createClass,但是如果读者你真的要用的话,需要注意关于getInitialState只出现在装载过程中,也就是说在一个组件的整个生命周期过程中,这个函数只被调用一次,不要在里面放置预期会被多次执行的代码。

  1. render

render函数无疑是React组件中最重要的函数,一个React组件可以忽略其他所有函数都不实现,但是一定要实现render函数,因为所有React组件的父类React.Component类对除render之外的生命周期函数都有默认实现。

通常一个组件要发挥作用,总是要渲染一些东西,render函数并不做实际的渲染动作,它只是返回一个JSX描述的结构,最终由React来操作渲染过程。

当然,某些特殊组件的作用不是渲染界面,或者,组件在某些情况下选择没有东西可渲染,那就让render函数返回一个null或者false,等于告诉React,这个组件这次不需要渲染任何DOM元素。

需要注意,render函数应该是一个纯函数,完全根据this.state和this.props来决定返回的结果,而且不要产生任何副作用。在render函数中去调用this.setState毫无疑问是错误的,因为一个纯函数不应该引起状态的改变。

  1. componentWillMount和componentDidMount

在装载过程中,componentWillMount会在调用render函数之前被调用,component-DidMount会在调用render函数之后被调用,这两个函数就像是render函数的前哨和后卫,一前一后,把render函数夹住,正好分别做render前后必要的工作。

不过,我们通常不用定义componentWillMount函数,顾名思义,componentWillMount发生在“将要装载”的时候,这个时候没有任何渲染出来的结果,即使调用this.setState修改状态也不会引发重新绘制,一切都迟了。换句话说,所有可以在这个component-WillMount中做的事情,都可以提前到constructor中间去做,可以认为这个函数存在的主要目的就是为了和componentDidMount对称。

而componentWillMount的这个兄弟componentDidMount作用就大了。需要注意的是,render函数被调用完之后,componentDidMount函数并不是会被立刻调用,componentDidMount被调用的时候,render函数返回的东西已经引发了渲染,组件已经被“装载”到了DOM树上。

componentWillMount和componentDidMount这对兄弟函数还有一个区别,就是componentWillMount可以在服务器端被调用,也可以在浏览器端被调用;而componentDidMount只能在浏览器端被调用,在服务器端使用React的时候不会被调用。

更新过程

当组件被装载到DOM树上之后,用户在网页上可以看到组件的第一印象,但是要提供更好的交互体验,就要让该组件可以随着用户操作改变展现的内容,当props或者state被修改的时候,就会引发组件的更新过程。

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate
  1. componentWillReceiveProps(nextProps)

关于这个componentWillReceiveProps存在一些误解。在互联网上有些教材声称这个函数只有当组件的props发生改变的时候才会被调用,其实是不正确的。实际上,只要是父组件的render函数被调用,在render函数里面被渲染的子组件就会经历更新过程,不管父组件传给子组件的props有没有改变,都会触发子组件的componentWill-ReceiveProps函数。

注意,通过this.setState方法触发的更新过程不会调用这个函数,这是因为这个函数适合根据新的props值(也就是参数nextProps)来计算出是不是要更新内部状态state。更新组件内部状态的方法就是this.setState,如果this.setState的调用导致componentWillReceiveProps再一次被调用,那就是一个死循环了。

  1. shouldComponentUpdate(nextProps, nextState)

除了render函数,shouldComponentUpdate可能是React组件生命周期中最重要的一个函数了。说render函数重要,是因为render函数决定了该渲染什么,而说shouldComponent-Update函数重要,是因为它决定了一个组件什么时候不需要渲染。

render和shouldComponentUpdate函数,也是React生命周期函数中唯二两个要求有返回结果的函数。render函数的返回结果将用于构造DOM对象,而shouldComponent-Update函数返回一个布尔值,告诉React库这个组件在这次更新过程中是否要继续。

在更新过程中,React库首先调用shouldComponentUpdate函数,如果这个函数返回true,那就会继续更新过程,接下来调用render函数;反之,如果得到一个false,那就立刻停止更新过程,也就不会引发后续的渲染了。

说shouldComponentUpdate重要,就是因为只要使用恰当,他就能够大大提高React组件的性能,虽然React的渲染性能已经很不错了,但是,不管渲染有多快,如果发现没必要重新渲染,那就干脆不用渲染好了,速度会更快。

我们知道render函数应该是一个纯函数,这个纯函数的逻辑输入就是组件的props和state。所以,shouldComponentUpdate的参数就是接下来的props和state值。如果我们要定义shouldComponentUpdate,那就根据这两个参数,外加this.props和this.state来判断出是返回true还是返回false。

如果我们给组件添加shouldCompomentUpdate函数,那就沿用所有React组件父React.Component中的默认实现方式,默认实现方式就是简单地返回true,也就是每次更新过程都要重新渲染。当然,这是最稳妥的方式,大不了浪费一点,但是绝对不会出错。不过若我们要追求更高的性能,就不能满足于默认实现,需要定制这个函数shouldComponentUpdate。

  1. componentWillUpdate和componentDidUpdate

如果组件的shouldComponentUpdate函数返回true, React接下来就会依次调用对应组件的componentWillUpdate、render和componentDidUpdate函数。

componentWillMount和componentDidMount, componentWillUpdate和componentDid-Update,这两对函数一前一后地把render函数夹在中间。

和装载过程不同的是,当在服务器端使用React渲染时,这一对函数中的Did函数,也就是componentDidUpdate函数,并不是只在浏览器端才执行的,无论更新过程发生在服务器端还是浏览器端,该函数都会被调用。

卸载过程

React组件的卸载过程只涉及一个函数componentWillUnmount,当React组件要从DOM树上删除掉之前,对应的componentWillUnmount函数会被调用,所以这个函数适合做一些清理性的工作。

和装载过程与更新过程不一样,这个函数没有配对的Did函数,就一个函数,因为卸载完就完了,没有“卸载完再做的事情”。

不过,componentWillUnmount中的工作往往和componentDidMount有关,比如,在componentDidMount中用非React的方法创造了一些DOM元素,如果撒手不管可能会造成内存泄露,那就需要在componentWillUnmount中把这些创造的DOM元素清理掉。