全面解读React生命周期

278 阅读8分钟

1、新旧生命周期

渲染工作流:组件数据改变到组件实际更新发生的过程。

1.1 React15生命周期简介(不重点关注)

constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()

img

1.2 React16生命周期解读

image.png

1.2.1 constructor

constructor 在类组件创建实例时调用,而且初始化的时候执行一次,可以在 constructor 做一些初始化的工作。

constructor(props){
    super(props)        // 执行 super ,别忘了传递props,才能在接下来的上下文中,获取到props。
    this.state={       //① 可以用来初始化state,比如可以用来获取路由中的
        name:'alien'
    }
    this.handleClick = this.handleClick.bind(this) /* ② 绑定 this */
    this.handleInputChange = debounce(this.handleInputChange , 500) /* ③ 绑定防抖函数,防抖 500 毫秒 */
    const _render = this.render
    this.render = function(){
        return _render.bind(this)  /* ④ 劫持修改类组件上的一些生命周期 */
    }
}

constructor 作用:

  • 初始化 state ,比如可以用来截取路由中的参数,赋值给 state 。
  • 对类组件的事件做一些处理,比如绑定 this , 节流,防抖等。
  • 对类组件进行一些必要生命周期的劫持,渲染劫持。

1.2.2 getDerivedStateFromProps

// nextProps 父组件新传递的 props ;
// prevState 组件在此次更新前的 state 。

getDerivedStateFromProps(nextProps,prevState)

只要组件更新,就会执行 getDerivedStateFromProps,不管是 props 改变,还是 setState ,或是 forceUpdate 。 ​

getDerivedStateFromProps 作用:

  • 代替 componentWillMount 和 componentWillReceiveProps
  • 组件初始化或者更新时,将 props 映射到 state。
  • 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。(请不要把 getDerivedStateFromProps 和 shouldComponentUpdate 强行关联到一起,两者没有必然联系)

getDerivedStateFromProps 直接被定义为 static 方法 —— static 方法内部拿不到组件实例的 this,这就导致你无法在 getDerivedStateFromProps 里面做任何类似于 this.fetch()、不合理的 this.setState(会导致死循环的那种)这类可能会产生副作用的操作。

1.2.3 render

可以在render里面做一些,createElement创建元素 , cloneElement 克隆元素React.children 遍历 children 的操作。 ​

1.2.4 componentDidMount

componentDidMount(){}

作用:

  • 可以做一些关于 DOM 操作,比如基于 DOM 的事件监听器。
  • 对于初始化向服务器请求数据,渲染视图,这个生命周期也是蛮合适的。

1.2.5 shouldComponentUpdate

shouldComponentUpdate(newProps,newState,nextContext){}

shouldComponentUpdate 三个参数,第一个参数新的 props ,第二个参数新的 state ,第三个参数新的 context 。 ​

作用:

  • 这个生命周期,一般用于性能优化,shouldComponentUpdate 返回值决定是否重新渲染的类组件。需要重点关注的是第二个参数 newState ,如果有 getDerivedStateFromProps 生命周期 ,它的返回值将合并到 newState ,供 shouldComponentUpdate 使用。

1.2.6 getSnapshotBeforeUpdate

// prevProps更新前的props ;
// preState更新前的state;

getSnapshotBeforeUpdate(prevProps,preState){
  return ...
}

getSnapshotBeforeUpdate 将返回一个值作为一个snapShot(快照),传递给 componentDidUpdate作为第三个参数。 ​

作用:

  • getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。 ​
getSnapshotBeforeUpdate(prevProps, prevState) {
  return ...
}
componentDidUpdate(preProps, preState, valueFromSnapshot) {
    console.log("componentDidUpdate方法执行");
    console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
  }

1.2.7 componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot){}

三个参数:

  • prevProps 更新之前的 props ;
  • prevState 更新之前的 state ;
  • snapshot 为 getSnapshotBeforeUpdate 返回的快照,可以是更新前的 DOM 信息;

作用:

  • componentDidUpdate 生命周期执行,此时 DOM 已经更新,可以直接获取 DOM 最新状态。这个函数里面如果想要使用 setState ,一定要加以限制,否则会引起无限循环。
  • 接受 getSnapshotBeforeUpdate 保存的快照信息。

1.2.8 componentWillUnmount

componentWillUnmount(){
    clearTimeout(this.timer)  /* 清除延时器 */
    this.node.removeEventListener('click',this.handerClick) /* 卸载事件监听器 */
}

componentWillUnmount 是组件销毁阶段唯一执行的生命周期,主要做一些收尾工作,比如清除一些可能造成内存泄漏的定时器,延时器,或者是一些事件监听器。 ​

作用:

  • 清除延时器,定时器。
  • 一些基于 DOM 的操作,比如事件监听器。

1.3 新增生命周期特点解读

getDerivedStateFromProps 不是 componentWillMount 的替代品,它仅有一个用途:使用 props 来派生/更新 state

getDerivedStateFromProps 在更新和挂载两个阶段都会使用;

在使用getDerivedStateFromProps,需要把握三个重点。

  • getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。
  • 该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。
  • getDerivedStateFromProps 需要一个对象格式的返回值。如果你没有指定这个返回值,那么大概率会被 React 警告。

getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。

getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新。

2、Fiber 架构

Fiber 会使原本同步的渲染过程变成异步的。

2.1 单线程的 JavaScript 与多线程的浏览器

JavaScript 是单线程的,浏览器是多线程的。

对于多线程的浏览器来说,它除了要处理 JavaScript 线程以外,还需要处理包括事件系统、定时器/延时器、网络请求等各种各样的任务线程,这其中,自然也包括负责处理 DOM 的UI 渲染线程。而 JavaScript 线程是可以操作 DOM 的。

JavaScript 线程和渲染线程必须是互斥的,必须串行,当其中一个线程执行时,另一个线程只能挂起等待。

如果JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,带给用户的体验就是所谓的卡顿。

2.2 产生卡顿的原因

协调所带来的一个问题是JavaScript 对主线程的超时占用问题。协调是一个同步的递归过程,不可以被打断。当处理结构相对复杂、体量相对庞大的虚拟 DOM 树时,协调需要的调和时间会很长,这就意味着 JavaScript 线程将长时间地霸占主线程,进而导致渲染卡顿/卡死、交互长时间无响应等问题。

2.3 Fiber 是如何解决问题的

Fiber 架构的应用目的是实现“增量渲染”。通俗来说就是把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面。实现增量渲染的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用户体验。

2.4 Fiber架构对生命周期的影响

  • render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。
  • pre-commit 阶段:可以读取 DOM。
  • commit 阶段:可以使用 DOM,运行副作用,安排更新。

总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。

由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,用户是可见的。

3、为什么要废除旧的生命周期

在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。

都处于 render 阶段,以下方法都可能重复被执行。

  • componentWillMount;
  • componentWillUpdate;
  • componentWillReceiveProps。

componentWill开头的生命周期里,习惯做的错误操作

  • setState();
  • fetch 发起异步请求;
  • 操作真实 DOM。

如何避免

(1)完全可以转移到其他生命周期(尤其是 componentDidxxx)里去做。

比如在 componentWillMount 里发起异步请求。componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。

(2)在 Fiber 带来的异步渲染机制下,可能会导致非常严重的 Bug。

假如你在 componentWillxxx 里发起了一个付款请求。由于 render 阶段里的生命周期都可以重复执行,在 componentWillxxx 被打断 + 重启多次后,就会发出多个付款请求。

getDerivedStateFromProps 为何会在设计层面直接被约束为一个触碰不到 this 的静态方法,其背后的原因也就更加充分了——避免开发者触碰 this,就是在避免各种危险的骚操作。

(3)避免滥用

比如在 componentWillReceiveProps  和 componentWillUpdate 里滥用 setState 导致重复渲染死循环。

4、结论

React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。

针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。 ​

5、问答

1、问:当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps 执行? ​

答案是否定的,componentWillReceiveProps 生命周期的执行,和纯组件没有关系,纯组件是在 componentWillReceiveProps 执行之后浅比较 props 是否发生变化。所以 PureComponent 下不会阻止该生命周期的执行。 ​

2、问:React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ? ​

答:useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。