深入浅出React和Redux
上QQ阅读APP看书,第一时间看更新

2.3.1 装载过程

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

□ constructor

□ getInitialState

□ getDefaultProps

□ componentWillMount

□ render

□ componentDidMount

我们来逐个地详细解释这些函数的功能。

1. constructor

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

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

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

□ 绑定成员函数的this环境。

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

以Counter组件为例,我们的构造函数有这样如下的代码:

this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
this.onClickDecrementButton = this.onClickDecrementButton.bind(this);

这两条语句的作用,就是通过bind方法让当前实例中onClickIncrementButton和onClickDecrementButton函数被调用时,this始终是指向当前组件实例。

注意

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

this.foo = ::this.foo;

等同于下面的语句:

this.foo = this.foo.bind(this);

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

2. 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只出现在装载过程中,也就是说在一个组件的整个生命周期过程中,这个函数只被调用一次,不要在里面放置预期会被多次执行的代码。

3. 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毫无疑问是错误的,因为一个纯函数不应该引起状态的改变。我们在后面的章节会对render函数做详细的介绍。

4. 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树上。

我们还是以ControlPanel为例,在ControlPanel中有三个Counter组件,我们稍微修改Counter的代码,让装载过程中所有生命周期函数都用console.log输出函数名和caption的值,比如,componentWillMount函数的内容如下:

componentWillMount() {
  console.log('enter componentWillMount ' + this.props.caption);
}

上面修改并没有添加任何功能,只是通过console.log输出一些内容,然后我们刷新网页,在浏览器的console里我们能够看见:

enter constructor: First

enter componentWillMount First

enter render First
enter constructor: Second
enter componentWillMount Second
enter render Second
enter constructor: Third
enter componentWillMount Third
enter render Third
enter componentDidMount First
enter componentDidMount Second
enter componentDidMount Third

可以清楚地看到,虽然componentWillMount都是紧贴着自己组件的render函数之前被调用,componentDidMount可不是紧跟着render函数被调用,当所有三个组件的render函数都被调用之后,三个组件的componentDidMount才连在一起被调用。

之所以会有上面的现象,是因为render函数本身并不往DOM树上渲染或者装载内容,它只是返回一个JSX表示的对象,然后由React库来根据返回对象决定如何渲染。而React库肯定是要把所有组件返回的结果综合起来,才能知道该如何产生对应的DOM修改。所以,只有React库调用三个Counter组件的render函数之后,才有可能完成装载,这时候才会依次调用各个组件的componentDidMount函数作为装载过程的收尾。

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

到目前为止,我们构造的React应用例子都只在浏览器端使用React,所以看不出区别,但后面第12章关于“同构”应用的介绍时,我们会探讨在服务器端使用React的情况。

至于为什么只有componentDidMount仅在浏览器端执行,这是一个实现上的决定,而不是设计时刻意为之。不过,如果非要有个解释的话,可以这么说,既然“装载”是一个创建组件并放到DOM树上的过程,那么,真正的“装载”是不可能在服务器端完成的,因为服务器端渲染并不会产生DOM树,通过React组件产生的只是一个纯粹的字符串而已。

不管怎样,componentDidMount只在浏览器端执行,倒是给了我们开发者一个很好的位置去做只有浏览器端才做的逻辑,比如通过AJAX获取数据来填充组件的内容。

在componentDidMount被调用的时候,组件已经被装载到DOM树上了,可以放心获取渲染出来的任何DOM。

在实际开发过程中,可能会需要让React和其他UI库配合使用,比如,因为项目前期已经用jQuery开发了很多功能,需要继续使用这些基于jQuery的代码,有时候其他的UI库做某些功能比React更合适,比如d3.js已经支持了丰富的绘制图表的功能,在这些情况下,我们不得不考虑如何让React和其他UI库和平共处。

以和jQuery配合为例,我们知道,React是用来取代jQuery的,但如果真的要让React和jQuery配合,就需要是利用componentDidMount函数,当componentDidMount被执行时,React组件对应的DOM已经存在,所有的事件处理函数也已经设置好,这时候就可以调用jQuery的代码,让jQuery代码在已经绘制的DOM基础上增强新的功能。

在componentDidMount中调用jQuery代码只处理了装载过程,要和jQuery完全结合,又要考虑React的更新过程,就需要使用下面要讲的componentDidUpdate函数。