React生命周期函数

  挂载阶段

  constructor -> static getDerivedStateFromProps -> render -> componentDidMount

  constructor(构造函数):初始化状态,当然它还接受一个props参数,可以使用组件传递过来的props,通常是用props初始化state。

  getDerivedStateFromProps: 被static 修饰,静态方法,这也意味着,在函数里面不能使用this和实例方法。它接受两个参数 props 和state,在渲染之前,根据props计算得到state,并返回state。

  render(渲染):组件中render 函数,它是至关重要的,因为组件的目的就是要渲染内容到页面上,不过它返回是一个虚拟DOM, ReactDOM 根据它构建出真实的DOM渲染到页面上。它会根据props 和state 渲染出react element, 指导react渲染出真正的dom. 但总存在特殊的情况,组件不需要渲染什么东西,如验证用户有没有登录的组件,它确实不需要返回什么,我们只是根据它进行跳转到不同的页面,这时直接在render 函数中返回null.

  componentDidMount(挂载完成):当真实的DOM 渲染到页面上会触发它。 需要注意的是render 函数在调用完成后,componentDidMount 并不会立刻调用,而是在当所有组件的render函数都调用完华之后,每个组件的componentDidMount 才会调用。 写一下代码会看得比较清楚,这也是我第一次明白地认识了react的挂载生命周期。

  写一个Counter 和一个CounterWrapper 组件, CounterWrapper包含Counter 。CounterWrapper 为父组件,Counter 为子组件。为了更能清楚地明白生命周期函数,CounterWrapper组件多包含几个Counter. 为了对每一个Counter 进行区分,分别给它们传一个caption 和initValue 参数。 在CounterWrapper 中 由于没有state, 我只写了render函数和ComponentDidMount 函数,而在子组件Counter中,它有state, 有完整的生命周期函数,依次写了四个阶段,constructor, static getDerivedStateFromProps, render, componentDidMount, 当然这里只是console.log一下信息,看一下执行顺序

// 子组件Counter
class Counter extends React.Component {
    constructor(props) {
        super(props);
        console.log(`进入子组件Counter的构造函数constructor ----- ${props.caption}`);
        this.state = {
            count: props.initValue || 0
        }
    }

    static getDerivedStateFromProps(props) {
        console.log(`进入子组件Counter的getDerivatedStateFromProps ----- ${props.caption}`);
    }
    render() {
        console.log(`进入子组件Counter的render ----- ${this.props.caption}`);
        const buttonStyle = {
            margin: '10px'
        };
        return (
            <div>
<button style={buttonStyle} >+</button> <button style={buttonStyle} value={5}>-</button> <span>{this.props.caption}count: {this.state.count}</span> </div> ); } componentDidMount() { console.log(`进入子组件Counter的componentDidMount ----- ${this.props.caption}`); } } // 父组件CounterWrapper class CounterWrapper extends React.Component { render(){ console.log('进入父组件CounterWrapper的render 函数'); return (<div> <Counter caption="第一个" initValue={3}></Counter> <Counter caption="第二个" initValue={6}></Counter> <Counter caption="第三个" initValue={9}></Counter> </div>) } componentDidMount() { console.log('进入父组件CounterWrapper的 componentDidMount 函数'); } }

  打开控制台,重点看一下componentDidMount。先进入的是父组件CounterWrapper 的render 函数,然后再进入第一个子组件的constructor, getDerivatedStateFromProps和 render, 再进入第二个子组件的constructor, getDerivatedStateFromProps和 render函数,再进入第三个子组件的constructor, getDerivatedStateFromProps和 render函数, 最后才是每一个子组件的componentDidMount, 第二个子组件的ccomponentDidMount函数, 第三个子组件的ccomponentDidMount函数,最后的最后是父组件的CompoentDidMount.

  组件的挂载生命周期并不是依次调用的,render 函数后面并不是紧紧根随着componentDidMount 函数。只有当三个组件的render函数都调用完成后,三个组件的componentDidMount 才连在一起被调用。 只有当所有的子组件的componentDidMount都调用了,父组件的componetDidMount 才会被调用。在componentDidMount 函数的调用上,我一直存在误解,以为render 函数调用后,立即执行它,并认为父组件的componentDidMount优于子组件的componentDidMount 执行,真是太想当然了。

  现在再来想一下为什么会有这样的设计,当使用componentDidMount 的时候,我们听得最多的一句话可能就是,在这里,你可以操作真实的DOM了,换句话说,当 组件compoentDidMount的时候,真实的DOM 已经渲染完毕了,整个页面都已经渲染完成了。整个页面就意味着整个DOM树都已经构建完成了,注意这里是整个DOM树。当React进入到组件的render 函数时,它只知道当前组件长什么样子,而不会知道整个DOM树长什么样子,所以它只能接着找到第二个组件render, 因为后面有第三个子组件,它还是不知道整个DOM树长什么样子,所以它还要找到第三个组件render, 也就是说它会把所有的组件都循环一遍,直到能够构建整个DOM树,它才会渲染真实的DOM, 这也是最为省事的办法,如果它找到一个渲染一个,后面的组件再对前面的组件有影响,它要把前面的做法再来一次,效率低下。一次渲染就OK了。 渲染之后,肯定是最深层的组件先完成,只有小的完成了才能组成大的,所以最深层的组件的componentDidMount 先执行, 最外层的最后执行。这让我们想成了JS事件处理阶段,当构建DOM树的过程中,它是捕获阶段,从外面找到最里面,而在渲染真实的DOM的时候,componentDidMount的时候,它是冒泡阶段, 从最里面到最外面。

  更新阶段

  static getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

  getDerivedStateFromProps: 和挂载阶段用法是一致的, 它适应用一种情况:组件的状态一直受父组件props的影响, 具体看下面的实例,css 用的是bootstrap的,npm install bootstrap 或 直接cdn link。

// 子组件DirectionDisplay
export class DirectionDisplay extends React.Component {
    state = {
        direction: "up",
        lastValue: 0
    }
    getClasses() {
        return (this.state.direction === "up" ? "bg-success" : "bg-danger")
            + " text-white text-center p-2 m-2";
    }
    render() {
        return <h5 className={this.getClasses()}>
            {this.props.value}
        </h5>
    }
    /* 
    接受props和state(当前组件的), 返回state. 当前组件的state只有两个属性lastValue, direction, 所以在函数返回state的时候,最好也返回这两个属性
    这里要注意一点,使用if 判断,如果props没有变化的话,直接返回state,
    因为update 阶段,也会调用这个生命周期函数。
    */
    static getDerivedStateFromProps(props, state) {
        if (props.value !== state.lastValue) {
            return {
                lastValue: props.value,
                direction: state.lastValue > props.value ? "down" : "up"
            }
        }
        return state;
    }
}

// 父组件
class App extends React.Component {
    state = {counter: 100}
    changeCounter = (val) => {
        this.setState({ counter: this.state.counter + val })
    }
    render() {
        return <div className="container text-center">
            <DirectionDisplay value={this.state.counter} />
            <div className="text-center">
                <button className="btn btn-primary m-1"
                    onClick={() => this.changeCounter(-1)}>Decrease</button>
                <button className="btn btn-primary m-1"
                    onClick={() => this.changeCounter(1)}>Increase</button>
            </div>
        </div >
    }
}

  shouldComponentUpdate() : 组件应不应该更新,它决定了一个组件是不是需要重新渲染, 如果它返回fasle, 就表示该组件不需要更新,也就不会重新渲染,它后面的生命周期函数就不会被执行。如果返回true,表示该组件需要更新, 它后面的生命周期函数才会被执行,重新渲染。 接受两个参数nextProps和nextState, 可以判断它们是不是和当前的props 和state 一致,如果一致,就是返回true,表示更新,如果不一致,则返回false,就不更新了。 现在来写一下componentShouldUpdate

    // 如果父组件传递过来的value发生变化,才会返回true, 组件才需要演染。
    shouldComponentUpdate(nextProps, nextState) {
        return (nextProps.value!== this.props.value);
    }

  render 就是渲染组件了,这没有什么可说的。getSnapshotBeforeUpdate,在更新之前得到快照,在使用refs的时候会用到,一般也不会用到。 componentDidUpdate, 和componentDidMount 用法一致

  卸载阶段,

  componentwillUnmount 函数,当React 组件要从DOM树上删掉之前,对应的componentWillUnmount函数就会被调用,它适合做一些清理性的工作,就是删除这个组件之前有没有什么要清理的。注意它没有对应的did函数,因为组件删除以后,没有什么事情可以做了。