深入浅出React+Redux(二:React 组件的生命周期)

时间:2022-10-10 16:23:03

前言

如同人有生老病死 , 自然界有日月更替,每个组件在网页中也会被创建、更新和删除,如同有生命的机体一样 。

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

  • 装载过程( Mount),也就是把组件第一次在 DOM 树中渲染的过程;
  • 更新过程( Update ),当组件被重新渲染的过程;
  • 卸载过程( Unmount),组件从 DOM 中删除的过程 。

三种不同的过程, React 库会依次调用组件的一些成员函数,这些函数称为生命周期函数。

(一)装载过程

当组件第一次被渲染的时候,依次调用的函数

  1. constructor: 构造函数,要创造一个组件类的实例。

    • 用于继承父类的props
    • 初始化state
    • 绑定成员函数的this
  2. getlnitialState: 函数的返回值会用来初始化组件的 this.state ,在一个组件声明周期只调用一次,不建议放置多次调用代码。但是,这个方法只有用 React.createClass 方法创造的组件类才会发生作用(PS:现在已经被遗弃,ES声明在构造函数中)。

  3. getDefaultProps:函数的返回值可以作为 props 的初始值。但是,这个函数只在 React.createClass 方法创造的组件类才会用到 (PS:现在已经被遗弃,ES声明使用组件的 defaultProps属性)。

  4. componentWillMount:将要装载时。即使调用 this.setState修改状态也不会引发重新绘制。换句话说,所有可以在这个 componentWillMount 中做的事情,都可以提前到 constructor 中间去做,可以认为这个函数存在的主要目的就是为了和 componentDidMount 对称 。

  5. render: 返回渲染内容。没有也必须返回null或者false,告诉React不需要渲染DOM元素。

  6. componentDidMount:组件加载完成。render 函数被调用完之后,并非立即调用,是render函数返回的内容已经渲染到DOM树上。此时才触发此函数。

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

    constructor(props){
....
+ console.log(`enter constructor, ${this.props.caption}`);
}

+ componentWillMount() {
+ console.log(`enter componentWillMount,${this.props.caption}`);
+ }


+ componentDidMount() {
+ console.log(`enter componentDidMount,${this.props.caption}`);
+ }

render(){
- console.log('render');
+ console.log(`enter render, ${this.props.caption}`);
...
}

现在我们看打印结果:

深入浅出React+Redux(二:React 组件的生命周期)

上面代码可以看出:componentWillMount紧跟render函数。但只有当所有三个组件的render 函数都被调用之后, 三个组件的 componentDidMount才连在一起被调用 。

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

componentWillMount和componentDidMount 的区别

componentWillMount 可以在服务器端被调用,也可以在浏览器端被调用
而 componentDidMount只能在浏览器端被调用,在服务器端使用 React 的时候不会被调用 。

PS:

componentDidMount类似window.onload。因为项目前期已经用 jQuery 开发了很多功能,需要继续使用这些基于 jQuery 的代码,就需要是利用 componentDidMount 函数。但是 componentDidMount 中调用 jQuery 代码只处理了装载过程,要和 jQuery 完全结合,又要考虑 React 的更新过程,就需要使用下面要讲的 componentDidUpdate 函数 。

(二)更新过程

当组件当 props 或者state 被修改的时候,就会引发组件的更新过程 ,依次调用的函数(但是不是所有更新过程都会执行全部函数)。

  1. componentWillReceiveProps
  2. shouldComponentUpdate
  3. componentWillUpdate
  4. render
  5. componentDidUpdate

    (1)componentWillReceiveProps(nextProps)
    只要是父组件的 render 函数被调用,在 render 函数里面被谊染的子组件就会经历更新过程,不管父组件传给子组件的 props 有没有改变,都会触发子组件的 componentWillReceiveProps 函数 。

// src/views/ControlPanel/index.jsx
render(){
return (
<Counter caption="Third" />
+ <button onClick={ () => this.forceUpdate () }>
+ Click me to repaint!
+ </button>
</div>
)
}

// src/views/ControlPanel/Counter.jsx

constructor(props){
....
- console.log(`enter constructor, ${this.props.caption}`);
+ // console.log(`enter constructor, ${this.props.caption}`);
}




- componentWillMount() {

+ /*componentWillMount() {

console.log(`enter componentWillMount,${this.props.caption}`);
- }

+ }*/



- componentDidMount() {

+ /*componentDidMount() {
console.log(`enter componentDidMount,${this.props.caption}`);
+ }*/

+ componentWillReceiveProps (nextProps) {
+ console.log(`enter componentWillReceiveProps,${nextProps.caption}`);
}

render(){
- console.log(`enter render, ${this.props.caption}`);
+ console.log(`ControlPanel render, ${this.props.caption}`);
...
}

现在我们看打印结果:

深入浅出React+Redux(二:React 组件的生命周期)

上面代码中,nextProps 代表的是这一次渲染传入的 props 值, this.props 代表的上一次渲染时的 props 值,在父容器调用 this .forceUpdate 重绘,但是props并未更改(每个 React 组件都可以通过 forceUpdate 函数强行引发一次重新绘制 )。

但是当我们点击第一个组件的+ 按钮时。可以看见控制台打印为:

ControlPanel render, First // render函数打印结果
finish // 点击+ 按钮更新state的成功回调。

可以发现 this.setState 不会引发 componentWillReceiveProps函数 被调用 。

PS 所以在 React 的组件组合中,完全可以只渲染一个子组件,而其他组件完全不需要渲染,这是提高 React 性能的重要方式.

(2)shouldComponentUpdate(nextProps, nextState)

除了 render 函数, shouldComponentUpdate 可能是 React 组件生命周期中最重要的一
个函数了 。

因为 render 函数决定了该渲染什么,而说 shouldComponentUpdate 函数重要,是因为它决定了一个组件什么时候不需要渲染 。并且是唯一两个必须要返回值的函数。

shouldComponentUpdate 函数返回布尔值。默认为true,若为false,则后续的render函数不讲执行。使用得当能提高React性能。

现在给 Counter 组件类增加 shouldComponentUpdate 函数的定义

shouldComponentUpdate(nextProps , nextState) {
return (nextProps.caption !== this.props.caption) || (nextState.count !==this.state.count);
}

深入浅出React+Redux(二:React 组件的生命周期)

上面可以发现。点击父容器因为props一样。并未触发render函数重绘。但是修改子组件state引起render函数重绘。

(3)componentWillUpdate 和 componentDidUpdate

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

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

使用 React 做服务器端渲染时,基本不会经历更新过程,因为服务器端只需要产出 HTML 字符串,一个装载过程就足够产出 HTML了,所以正常情况下服务器端不会调用 componentDidUpdate 函数。

但是客户端,当 React 组件被更新时,原有的内容被重新绘制,这时候就需要在 componentDidUpdate 函数再次调用 jQuery 代码 。

(三)卸载过程

React 组件的卸载过程只涉及一个函数 componentWillUnmount

当 React 组件要从DOM 树上删除掉之前,对应的 componentWillUnmount 函数会被调用,所以这个函数适合做一些清理性的工作 。比如,在componentDidMount 中用非 React 的方法创造了一些 DOM 元素或者定时器之类,如果撒手不管可能会造成内存泄露,那就需要在 componentWillUnmount 中清理掉。

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

深入浅出React+Redux(二:React 组件的生命周期)

PS 此图是下载别人的。原文地址

(三)组件向外传递数据和实例

前文说过。在 JavaScript 中,函数是一等公民,函数本身就可以被看做一种对象,既可以像其他
对象一样作为 prop(prop可以是任意一种js的数据类型) 的值从父组件传递给子组件,又可以被子组件作为函数调用。

现在我们来写个demo。需要子组件更新后。在父组件底部同步显示count的总数。

代码太多。可以查看版本链接

代码很简单。就不解释呢。

运行图
深入浅出React+Redux(二:React 组件的生命周期)

(四)React 组件 state 和 prop 的局限

上面demo中。每个 Counter 组件有自己的状态记录当前计数,而父组件ControlPanel 也有一个状态来存储所有 Counter 计数总和,也就是说,数据发生了重复 。带来的一个问题就是如何保证重复的数据一致,如果数据存多份而且不一致,那就很难决定到底使用哪个数据作为正确结果了 。

一种方案: 以一个组件的state为准,将Counter变为stateless组件,只负责展示。ControlPanel 变为容器组件。将变化全部放在这个state内部。通过func来回调执行方案。但是这样会导致修改父容器的render函数。其他子组件全部更新(你也可以判断传入props实现子组件不更新)。但是这样一旦子组件过多怎么管理子组件数据?

另一种方案。就是加个顶层容器来记录所有的state。但是问题也出现?props需要传两层,不符合低耦合的设计规范。

这是就需要全局的状态管理。Flux,Redux或者MobX 。下一章会细说状态管理所解决的问题。