2.3.2 更新过程
您可以在百度里搜索“深入浅出React和Redux 艾草文学(www.321553.xyz)”查找最新章节!
2.3.2 更新过程
当组件被装载到DOM树上之后,用户在网页上可以看到组件的第一印象,但是要提供更好的交互体验,就要让该组件可以随着用户操作改变展现的内容,当props或者state被修改的时候,就会引发组件的更新过程。
更新过程会依次调用下面的生命周期函数,其中render函数和装载过程一样,没有差别。
·componentWillReceiveProps
·shouldComponentUpdate
·componentWillUpdate
·render
·componentDidUpdate
有意思的是,并不是所有的更新过程都会执行全部函数,下面会介绍到各种特例。
1.componentWillReceiveProps(nextProps)
关于这个componentWillReceiveProps存在一些误解。在互联网上有些教材声称这个函数只有当组件的props发生改变的时候才会被调用,其实是不正确的。实际上,只要是父组件的render函数被调用,在render函数里面被渲染的子组件就会经历更新过程,不管父组件传给子组件的props有没有改变,都会触发子组件的componentWill-ReceiveProps函数。
注意,通过this.setState方法触发的更新过程不会调用这个函数,这是因为这个函数适合根据新的props值(也就是参数nextProps)来计算出是不是要更新内部状态state。更新组件内部状态的方法就是this.setState,如果this.setState的调用导致component-WillReceiveProps再一次被调用,那就是一个死循环了。
让我们对ControlPanel做一些小的改进,来体会一下上面提到的规则。
我们首先在Counter组件类里增加函数定义,让这个函数componentWillReceive-Props在console上输出一些文字,代码如下:
componentWillReceiveProps(nextProps) {
console.log('enter componentWillReceiveProps ' + this.props.caption)
}
在ControlPanel组件的render函数中,我们也做如下修改:
render() {
console.log('enter ControlPanel render');
return (
. . .
this.forceUpdate() }>
Click me to repaint!
);
}
除了在ControlPanel的render函数入口处增加console输出,我们还增加了一个按钮,这个按钮的onClick事件引发一个匿名函数,当这个按钮被点击的时候,调用this.forceUpdate,每个React组件都可以通过forceUpdate函数强行引发一次重新绘制。
注意
类似上面的代码,在JSX用直接把匿名函数赋值给onClick的方法,看起来非常简洁而且方便,其实并不是值得提倡的方法。因为每次渲染都会创造一个新的匿名方法对象,而且有可能引发子组件不必要的重新渲染,原因在后面的章节会有详细介绍。
在网页中,我们去点击那个新增加的按钮,可以看到浏览器的console中有如下的输出:
enter ControlPanel render
enter componentWillReceiveProps First
enter render First
enter componentWillReceiveProps Second
enter render Second
enter componentWillReceiveProps Third
enter render Third
可以看到,引发forceUpdate之后,首先是ControlPanel的render函数被调用,随后第一个Counter组件的componentWillReceiveProps函数被调用,然后Counter组件的render函数被调用,随后第二第三个函数的这两个函数也依次被调用。
然而,ControlPanel在渲染三个子组件的时候,提供的props值一直就没有变化,可见componentWillReceiveProps并不是当props值变化的时候才被调用,所以,这个函数有必要把传入参数nextProps和this.props作必要对比。nextProps代表的是这一次渲染传入的props值,this.props代表的上一次渲染时的props值,只有两者有变化的时候才有必要调用this.setState更新内部状态。
在网页中,我们再尝试点击第一个Counter组件的“+”按钮,可以看到浏览器的console输出如下:
enter render First
明显,只有第一个组件Counter的render函数被调用,函数componentWillReceive-Props没有被调用。因为点击“+”按钮引发的是第一个Counter组件的this.setState函数的调用,就像上面说过的一样,this.setState不会引发这个函数componentWillReceive-Props被调用。
从这个例子中我们也会发现,在React的组件组合中,完全可以只渲染一个子组件,而其他组件完全不需要渲染,这是提高React性能的重要方式。
2.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。
让我们尝试来给Counter组件增加一个shouldComponentUpdate函数。先来看看props,Counter组件支持两个props,一个叫caption,一个叫initValue。很明显,只有caption这个prop改变的时候,才有必要重新渲染。对于initValue,只是创建Counter组件实例时用于初始化计数值,在组件实例创建之后,无论怎么改,都不应该让Counter组件重新渲染。
再来看看state,Counter组件的state只有一个值count,如果count发生了变化,那肯定应该重新渲染,如果count没变化,那就没必要了。
现在,让我们给Counter组件类增加shouldComponentUpdate函数的定义,代码如下:
shouldComponentUpdate(nextProps, nextState) {
return (nextProps.caption !== this.props.caption) ||
(nextState.count !== this.state.count);
}
现在,只有当caption改变,或者state中的count值改变,shouldComponent才会返回true。
值得一提的是,通过this.setState函数引发更新过程,并不是立刻更新组件的state值,在执行到到函数shouldComponentUpdate的时候,this.state依然是this.setState函数执行之前的值,所以我们要做的实际上就是在nextProps、nextState、this.props和this.state中互相比对。
我们在网页中引发一次ControlPanel的重新绘制,可以看到浏览器的console中的输出是这样:
enter ControlPanel render
enter componentWillReceiveProps First
enter componentWillReceiveProps Second
enter componentWillReceiveProps Third
可以看到,三个Counter组件的render函数都没有被调用,因为这个刷新没有改变caption的值,更没有引发组件内状态改变,所以完全没有必要重新绘制Counter。
对于Counter这个简单的组件,我们无法感觉到性能的提高,但是,实际开发中会遇到更复杂更庞大的组件,这种情况下避免没必要的重新渲染,就会大大提高性能。
3.componentWillUpdate和componentDidUpdate
如果组件的shouldComponentUpdate函数返回true,React接下来就会依次调用对应组件的componentWillUpdate、render和componentDidUpdate函数。
componentWillMount和componentDidMount,componentWillUpdate和componentDid-Update,这两对函数一前一后地把render函数夹在中间。
和装载过程不同的是,当在服务器端使用React渲染时,这一对函数中的Did函数,也就是componentDidUpdate函数,并不是只在浏览器端才执行的,无论更新过程发生在服务器端还是浏览器端,该函数都会被调用。
在介绍componentDidMount函数时,我们说到可以利用componentDidMount函数执行其他UI库的代码,比如jQuery代码。当React组件被更新时,原有的内容被重新绘制,这时候就需要在componentDidUpdate函数再次调用jQuery代码。
读者可能会问,componentDidUpdate函数不是可能会在服务器端也被执行吗?在服务器端怎么能够使用jQuery呢?实际上,使用React做服务器端渲染时,基本不会经历更新过程,因为服务器端只需要产出HTML字符串,一个装载过程就足够产出HTML了,所以正常情况下服务器端不会调用componentDidUpdate函数,如果调用了,说明我们的程序有错误,需要改进。 深入浅出React和Redux