react-生命周期

loading 2022年12月06日 169次浏览

先看旧生命周期的整体流程图

首先要注意,只有类组件有生命周期,函数式组件只能通过hooks实现类似的功能。

1.初始化阶段

1.1 componentWillMount

该函数在react16.2后已被建议弃用。因为老的diff算法在对比虚拟DOM和真实DOM时是同步的,万一对比量过大容易造成性能影响。
16.2推出了新的生命周期,更新了diff算法,提出了fiber技术。此时componentWillMount属于低优先级任务,容易被高优先级任务中断,需要重新执行,具有隐患。

第一次render之前最后一次修改状态的机会,主要作用是进行数据初始化

1.2 render

只能访问this.props和this.state,不允许修改状态和dom输出

1.3 componentDidMount

成功render并渲染完成真实dom后触发,可以修改dom。
适合用于

  • 数据请求 比如axios
  • 订阅函数调用
  • setInterval
  • 基于创建完的dom进行初始化

来看个案例,BetterScroll需要基于创建完的DOM来工作,因此写在componentDidMount中。

export default class App extends Component {
    state = {
        list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
    }

    UNSAFE_componentWillMount() {
        console.log(document.querySelectorAll('li'))//获取到一个空数组
    }

    render() {
        return (
            <div>
                <div id='wrapper' style={{height:'150px',background:'lightgreen',overflow:'hidden'}}>
                    <ul>
                        {
                            this.state.list.map(item =>
                                <li key={item}>{item}</li>
                            )
                        }
                    </ul>
                </div>
            </div>
        )
    }

    componentDidMount() {
        console.log(document.querySelectorAll('li'))//获取到10个li
        
        // 要在这里new 因为此时已经获取到dom节点
        new BetterScroll('#wrapper')
    }
}

2. 运行(更新)阶段

2.1 componentWillUpdate

用的机会不多,不像Mount可以用来初始化数据,在Update阶段初始化数据会导致死循环。(render后又update状态->状态修改后又触发render...)

类似于componentWillMount,也是不安全的,建议弃用。

2.2 render

和初始化阶段一样,只能访问this.props和this.state,不允许修改状态和dom输出

2.3 componentDidUpdate

类似componentDidMount,作用是更新后用来获取DOM节点

看个小例子,在componentDidMount中用axios异步获取数据时需要将betterscroll写在setState的第二个参数中作为回调函数执行。

    componentDidMount() {
        axios.get(`/test.json`).then(res => {
            this.setState({
                filmList: res.data.data.films
            }, () => {
                new BetterScroll('#wrapper')
            })
        })
    }

可以不在setState中写,直接在axios获取到数据并更新状态后的componentDidUpdate中写,效果相同。

componentDidUpdate也有对应的缺点,就是会执行多次,每次更新状态都会使得它重新执行。

    componentDidMount() {
        axios.get(`/test.json`).then(res => {
            this.setState({
                filmList: res.data.data.films
            })
        })
    }

    componentDidUpdate() {
        new BetterScroll('#wrapper')
    }

该生命周期可以传入两个参数,用于记录老的属性和老的状态

    componentDidUpdate(prevProps , prevState) {
        console.log(prevState.filmList) //空数组[]
        if(prevState.filmList.length === 0){
            new BetterScroll('#wrapper')
        }
    }

2.4 shouldComponentUpdate

如果该方法返回false,那么就会阻止render调用

举个例子,第一次点击按钮后myname会改变,但是之后每次点击按钮,虽然state值没有改变,但还是会继续触发虚拟dom和真实dom的对比,render重新渲染页面。

export default class App extends Component {
    state = {
        myname: 'yoimiya',
    }
    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        myname: 'eula'
                    })
                }}>click</button>

                <span id='myname'>{this.state.myname}</span>
            </div>
        )
    }
}

该生命周期也是可以传两个参数,分别代表新属性和新状态

    // this.state 老的状态
    // nextState 新的状态
    shouldComponentUpdate(nextProps , nextState){
        if(this.state.myname !== nextState.myname){
	    // 允许render调用
            return true;
        }else{
	    // 不允许render调用
            return false;
        }
    }

为了不一一对比对象中的属性,直接将对象序列化后对比两个整体对象。

    // this.state 老的状态
    // nextState 新的状态
    shouldComponentUpdate(nextProps , nextState){
        if(JSON.stringify(this.state) !== JSON.stringify(nextState)){
            return true;
        }else{
            return false;
        }
    }

该生命周期对于性能优化有着重要的作用,举个例子:一个父组件底下10个子组件,根据父组件状态修改其中1个子组件会导致剩下的也要重新渲染,大大影响性能。

先来看没用shouldComponentUpdate优化的代码,根据输入框的value修改current状态,将current和index传给子组件做对比,相等时背景色改变:

class Box extends Component {
    render() {
        return (
            <div style={{
                width: '100px', height: '100px', border: '1px solid grey', margin: '10px', float: 'left',
                // 根据条件来决定背景色
                background: this.props.current === this.props.index ? 'lightgreen' : ''
            }}>

            </div>
        )
    }
}

export default class App extends Component {
    state = {
        list: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
        current: 0
    }
    render() {
        return (
            <div>
                {/* 根据输入框内容修改current 记得用Number()转换 */}
                <input type='number' onChange={(e) =>{
                    this.setState({
                        current : Number(e.target.value)
                    })
                }}></input>

                <div style={{ overflow: 'hidden' }}>
                    { 
                        this.state.list.map((item, index) =>
                            // 将current和index传给子组件 
                            <Box key={item} current={this.state.current} index={index}></Box>
                        )
                    }
                </div>
            </div>
        )
    }
}

但是这份代码有个问题,每次修改current,10个Box都会被render重新渲染一次,造成性能影响。

因此可以在Box组件里加入这段代码,实现了每次render只更新2个(老的和新的)需要被更新的Box:

    shouldComponentUpdate(newProps){
        // 只允许render更新老的Box:选中->未选中,和新的Box:未选中->选中
        if(this.props.current === this.props.index || newProps.current === newProps.index){
            return true;
        }
        return false;
    }

2.5 componentWillReceiveProps

此生命周期也已经被弃用

父组件修改属性时触发,因此应该写在子组件内。

但是其实并不需要将属性传给孩子,哪怕只是在父组件中更新状态,也会导致子组件的生命周期更新,也会触发该方法。

这个生命周期唯一作用是最先获得父组件传来的属性,可以利用属性进行相应的操作

通过形参中的nextProps(名字不固定),来获取最新的属性

比如这个例子

class Child extends Component {
    render() {
        return (
            <div>
                child
            </div>
        )
    }
    componentWillReceiveProps(nextProps){
        console.log(this.props.text , nextProps.text)
        // 111 222
    }
}

export default class App extends Component {
    state = {
        text : '111'
    }
    render() {
        return (
            <div>
                {
                    this.state.text
                }
                <button onClick={() =>{
                    this.setState({
                        text : '222'
                    })
                }}>click</button>
                <Child text={this.state.text}></Child>
            </div>
        )
    }
}

3. 销毁阶段

销毁阶段就一个函数:componentWillUnmount

作用是在移除组件之前进行清理操作,比如计时器和事件监听器。

export default class App extends Component {
    state = {
        isCreated: true
    }
    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        isCreated: !this.state.isCreated
                    })
                }}>click</button>
                {this.state.isCreated && <Child />}
            </div>
        )
    }
}

class Child extends Component {
    render() {
        return <div>
            child
        </div>
    }

    componentDidMount() {
        window.onresize = () => {
            console.log("resize")
        }

	// this是Child组件的一个实例
        this.timer = setInterval(() => {
            console.log("111")
        }, 1000)
    }


    componentWillUnmount() {
        console.log("componentWillUnmount")

        // 清除掉初始化时设置的监听事件
        window.onresize = null

        // 清除掉定时器
        clearInterval(this.timer)
    }
}

4. 新生命周期

先看整体流程图

注意react16自上而下对生命周期做了另一种解读,render阶段,pre-commit阶段还有commit阶段

4.1 老生命周期的问题

(1) componentWillMount
在ssr中这个方法将会被多次调用,所以会重复触发多遍,同时在这里如果绑定事件将无法解绑,导致内存泄漏,变得不够安全高效逐步废弃。(还有上面提到的fiber算法导致低优先级的原因)

(2) componentWillReceiveProps

  1. 外部组件多次频繁更新传入多次不同的 props,会导致不必要的异步请求
  2. 旧的React中componentWillReceiveProps方法是用来判断前后两个props是否相同,如果不同,则将新的props更新到相应的state上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏state数据的单一数据源,导致组件状态变得不可预测。

(3) componetWillupdate
在React开启异步渲染模式后,在执行函数时读到的DOM元素状态并不总是渲染时相同,这就导致在componentDidUpdate中使用componentWillUpdate中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。

4.2 getDerivedStateFromProps

第一次初始化组件以及后续的更新过程中(包括自身状态更新以及父传子),返回一个对象作为新的state,返回null则说明不需要在这里更新state。

对比componentWilReceiveProps的优点在于禁止了组件去访问this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。

其次,它将所有的异步操作合并在一起返回一次state,避免了频繁传入props导致的不断异步请求引起的资源浪费。

下面来看看具体用法,体现出该函数在初始化时和后续更新时会被调用,可以通过nextState获取新状态,有两个注意点:

export default class App extends Component {
    state = {
        myname: 'yoimiya'
    }
    // // (1)需要写成静态方法(要注意静态方法里没有自己的this)
    static getDerivedStateFromProps(nextProps , nextState) {
        console.log(nextState) //初始为{myname:'yoimiya} 点击后为{myname:'eula}

        // (2)需要返回一个值作为新的state 可以为空 代表状态不变
        return {
            myname: nextState.myname.substring(0,1).toUpperCase() + nextState.myname.substring(1)
        }
    }
    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        myname: 'eula'
                    })
                }}>click</button>
                {this.state.myname}
            </div>
        )
    }
}}

在实际运用中,建议该生命周期只负责将传过来的props转化为自己的state,需要搭配componentDidUpdate()来执行。

具体代码看自己vscode里的文件吧('./03_lifecycle/37_gdsfpDemo') 太长了,其实就是把componentWillReceiveProps替换成getDerivedStateFromProps + componentDidUpdate

4.3 getSnapshotBeforeUpdate

取代了componetWillUpdate,解决了willUpdate和didUpdate之间时间间隔过长的问题。

触发时间为update发生的时候,在render之后didUpdate之前返回一个值,作为componentDidUpdate的第三个参数。

来看基本用法

export default class App extends Component {
    state = {
        text: 111
    }
    render() {
        console.log('render')
        return (
            <div>
                <button onClick={() =>{
                    this.setState({
                        text: 222
                    })
                }}>click</button>
                {this.state.text}
            </div>
        )
    }
    getSnapshotBeforeUpdate(){
        console.log('snapshot');
        // 必须得return点东西 作为参数传给DidUpdate
        return 100;
    }
    componentDidUpdate(prevProps , prevState , value){
        console.log('did update' , value)
    }

    //render
    //render
    //snapshot
    //did update 100
}

来看个具体的案例,邮箱来邮件之后让视窗仍然保持在当前在看的邮件,而非自动弹到最新邮件处。

export default class App extends Component {
    state = {
        list: [1, 2, 3, 4, 5]
    }
    myRef = React.createRef();
    render() {
        return (
            <div>
                <h1>邮箱Demo</h1>

                <div style={{ height: '200px', overflow: 'auto' }} ref={this.myRef}>
                    <ul>
                        {
                            this.state.list.map(item =>
                                <li key={item} style={{ height: '100px', background: 'lightgreen' }}>{item}</li>
                            )
                        }
                    </ul>
                </div>

                <button onClick={() => {
                    this.setState({
                        list: [...[11, 12, 13, 14, 15], ...this.state.list]
                    })
                }}>收邮件</button>
            </div>
        )
    }
    getSnapshotBeforeUpdate() {
        //获取未更新dom前容器高度
        return this.myRef.current.scrollHeight
    }
    componentDidUpdate(prevProps , prevState , value){
        // 将top值设为滚动高度差 成功实现“快照保存”邮件来前位置效果
        this.myRef.current.scrollTop += this.myRef.current.scrollHeight - value;
    }
}

5.PureComponent

顺带一提这个性能优化的问题,之前的shouldComponentUpdate需要自己去判断状态,属性等来决定组件的更新。

通过继承PureComponent可以通过一种叫shallowEqual的方法直接实现自动比较,也就是react帮你把活干了。

该方法可以应用于绝大部分场景,除非state或props一直会改变(比如倒计时),那么pureComponent就显得没有必要去比较新老状态是否改变了,反而会因为shallowEqual影响性能。

使用方法是直接继承即可

// 注意这里的导入
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
    state = {
        myname: 'yoimiya',
    }
    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        myname: 'eula'
                    })
                }}>click</button>

                <span id='myname'>{this.state.myname}</span>
            </div>
        )
    }

    // this.state 老的状态
    // nextState 新的状态
    // shouldComponentUpdate(nextProps, nextState) {
    //     if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
    //         return true;
    //     } else {
    //         return false;
    //     }
    // }
}

使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。

pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。