React新旧版本生命周期变化

3,009 阅读6分钟

React新旧版本生命周期变化

由于一些生命周期函数经常会被误解和滥用,同时React官方也为了实现后面新版本的异步渲染,废除了一些生命周期函数,同时也新增了一些生命周期函数

新旧生命周期版本对比

v16.3之前的生命周期图谱:

image-20200528113227949

v16.3版本的生命周期图谱:

image-20200528113856923

v16.4版本的生命周期只是改动了getDerivedStateFromProps()的调用情形, 只要是可能会触发组件更新的情况(New props, setState(), fourceUpdate()),就会调用该生命周期函数。

弃用的生命周期函数(UNSAFE)

1. componentWillMount()

一般开发者在这里常做三件事:初始化state、调用异步接口获取数据、订阅事件/添加事件监听器

该生命周期方法经常被误解和滥用;我们对它有哪些认知上的误解呢?

componentWillMountcomponentWillUnMount并不是一定会一前一后成对出现的,不意味着同一组件内前者调用之后,后者一定会被调用。事实上,React会确保在成功调用componentDidMount()生命周期函数之后,随后才会调用componentWillUnMount进行内存清除相关操作 (内存泄漏

componentWillMount执行之后会立即调用render()函数,并不能保证在用户看到UI之前使那些调用异步接口的数据返回,也就不能解决初次渲染时空数据的问题。

由于React未来会推出新的渲染方式--异步渲染,一种生命周期可被打断的渲染方式,具体是在render()生成虚拟 dom 阶段可以打断重来, 这就会导致在dom被挂在之前或是被更新之前的所有任务都会重复操作,所以componentWillMount()、·componentWillReceiveProps() componentWIllUpdate()方法可能会执行多次。(函数内部逻辑多次调用

该方法是服务端渲染唯一一个会调用的生命周期函数。(服务端渲染时在这里声明的变量可能无法清除,内存泄漏)

基于以上,使用'componentWillMount()'会造成内存泄漏, 数据重复加载等问题;新版本中官方推荐将初始化的操作放在constructor()中, 将请求异步数据、订阅事件源、监听事件的操作放在componentDidMount()

2.componentWillReceiveProps(nextProps, nextContext)

在老版本的React中,如果组件自身的state与其props密切相关的话,我们就会用到componentWillReceiveProps(nextProps)。常见的业务场景比如,tabs的激活状态,一般我们会在组件自身内通过state维持,但是当我们从其他页面返回时,想要保持离开之前时的tabs状态,这时我们可以通过props来传递,(破坏了数据源的单一性)

//previous 
componentWillReceiveProps(nextProps, nextContext) {
   if(nextProps.activeIndex !== this.state.activeIndex) {
     this.setState({activeIndex: nextProps.activeIndex})
     this.fetchData() //因异步中断,可能会重复操作
   }
 }

/** next 
*将更新state与触发逻辑的操作分成两部分来执行,state更新部分在getDerivedStateFromProps中完成, 逻辑部分操作
*在componentDidUpdate()中完成
*/
static getDerivedStateFromProps(nextProps, prevState) { //此方法不能获取组件实例
   if(nextProps.activeIndex !== prevState.activeIndex) {
     return {activeIndex: nextProps.activeIndex}
   }
 }
componentDidUpdate() {
  this.fetchData() //可以确保只执行一次
}

该生命周期函数按照上面图谱中应该是在props属性改变之后调用,但其实只要父组件重新渲染,无论子组件的props有没有更新,子组件都会调用componentWillReceiveProps 注意这里可能会造成死循环,即当子组件在该方法中调用了父组件通过props传递过来的函数时, 恰巧该函数中有能让父组件重新渲染的逻辑,就会造成死循环

class Parent extends Component {
    render() {
        return (
            <div>
          			{/* 迫使父组件更新, 子组件就会调用componentWillReceiveProps */}
                <div onClick={() => this.forceUpdate()}> re-render </div> 
                <Child parentFun={() => this.setState({})} />
            </div>
        )
    }
}

class Child extends Component {
    componentWillReceiveProps(nextProps, nextContext) {
      {/*  该函数也会使父组件重新渲染,造成死循环 */}
        nextProps.parentFun()
    }
    render() {
        return (
            <div> child component </div>
        );
    }
}
image-20200605203801324

基于以上,使用componentWillReceiveProps()会照成方法多次调用React提出 static getDerivedStateFromProps()替代方案.

3.componentWillUpdate(nextProps, nextState)

该方法发生在组件渲染之前,初次渲染时不会调用

我们通常的认识是:由于该方法在render()之前调用, 如果把数据更新的操作放在componentDidUpdate()中就来不及了,因为该方法在render()之后调用嘛。但事实并非如此,React会确保用户在看到最新的UI之前,刷新componentWillUpdate()componentDidUpdate()期间发生的任何setState()调用,注意最好不要在这里直接进行setState的操作否则会导致进入死循环。

在组件更新前读取DOM元素状态 另一个操作就是在组件更新前获取dom元素状态,更新之后在componentDidUpdate中处理。但是此方法在render之前调用,当React开启异步渲染模式后,render之前的方法都可能会被多次调用,而且也不能够保证在这里获取的DOM元素状态是最近一次的。

新增的生命周期函数

1.static getDerivedStateFromProps(nextProps, prevState)

这是一个不会被子类继承的静态函数。会在组件每次渲染之前调用,该方法接收此次props,和当前还没更新state作为参数,返回一个对象用于更新state,如果返回null则不更新任何内容。该方法不可以获取组件实例this, React官方本身的目的也是为了让开发者认识到,但凡使用该方法就是为了处理组件内部state与props相关联的这一类问题。

2. getSnapshotBeforeUpdate(preProps, preState)

该函数发生在componentDidUpdate之前, 可以获取组件在更新之前且是最近一次的DOM元素状态, 返回一个更新前的快照snapshot,作为componentDidUpdate的第三个参数。这个生命周期函数不经常用到, 这里举一个实际中一般不会这么用的场景,仅是为了加深对该生命周期函数的理解

//需求: 每次调用fetchData时会加载数据,要求在数据每次加载后,页面能自动滚动到最新数据处
fetchData() {
  this.setState({count: this.state.count + 1})
  let arr = [1,2,3,4,5]
  this.setState({list: [...this.state.list, ...arr]})
 }
 getSnapshotBeforeUpdate(preProps, preState) {
   if(this.state.list.length > preState.list.length) {
     const current = document.getElementById("current")
     //获取上一帧内容的可见高度
     return current.scrollHeight - current.scrollTop 
   } else return null
 }
componentDidUpdate(preProps, preState, snapshort) {
  const current = document.getElementById("current")
  if(snapshort) {
    // 如果有新数据时,向上滚动直至完全隐藏上一帧内容可见的高度
    current.scrollTop = current.scrollHeight - snapshort
  }
}

总结

React将要废弃的这三个生命周期函数,大都是因为新版本的异步渲染,在调用render生成虚拟DOM阶段,由于更高级的操作到来而被打断,导致render之前的操作都会重来。使得之前的逻辑可能会被重复调用而弃用。

参考

异步渲染之更新