预习:生命周期的使用(只针对新生命周期)

1、constructor

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

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

constructor 作用:

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

2、getDerivedStateFromProps

  1. // nextProps 父组件新传递的 props ;
  2. // prevState 组件在此次更新前的 state 。
  3. getDerivedStateFromProps(nextProps,prevState)

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

getDerivedStateFromProps 作用:

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

3、render

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

4、componentDidMount

  1. componentDidMount(){}

作用:

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

5、shouldComponentUpdate

  1. shouldComponentUpdate(newProps,newState,nextContext){}

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

作用:

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

6、getSnapshotBeforeUpdate

  1. // prevProps更新前的props ;
  2. // preState更新前的state;
  3. getSnapshotBeforeUpdate(prevProps,preState){
  4. return ...
  5. }

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

作用:

  • getSnapshotBeforeUpdate 这个生命周期意义就是配合componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate 。保存一次更新前的信息。

7、componentDidUpdate

  1. componentDidUpdate(prevProps, prevState, snapshot){}

三个参数:

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

作用

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

8、componentWillUnmount

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

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

作用

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

一、生命周期背后的设计思想

组件和虚拟 DOM

1. 虚拟DOM

image.png

2. 组件化

在React中,每个组件既是“封闭”的,也是“开放”的。

所谓“封闭”,主要是针对“渲染工作流”(指从组件数据改变到组件实际更新发生的过程)来说的。在组件自身的渲染工作流中,每个组件都只处理它内部的渲染逻辑。在没有数据流交互的情况下,组件与组件之间可以做到“各自为政”。

而所谓“开放”,则是针对组件间通信来说的。React 允许开发者基于“单向数据流”的原则完成组件间的通信。而组件之间的通信又将改变通信双方/某一方内部的数据,进而对渲染结果构成影响。所以说在数据这个“红娘”的牵线搭桥之下,组件之间又是彼此开放的,是可以相互影响的。

这一“开放”与“封闭”兼具的特性,使得 React 组件既专注又灵活,具备高度的可重用性和可维护性。

二、新旧生命周期方法

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

1. React15生命周期

  1. constructor()
  2. componentWillReceiveProps()
  3. shouldComponentUpdate()
  4. componentWillMount()
  5. componentWillUpdate()
  6. componentDidUpdate()
  7. componentDidMount()
  8. render()
  9. componentWillUnmount()

image.png

2. React16生命周期

image.png

3. 新旧生命周期对比

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

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

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

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

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

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

4. 为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps?

React 16 在强制推行“只用 getDerivedStateFromProps 来完成 props 到 state 的映射”这一最佳实践。

  1. static getDerivedStateFromProps(props, state)

确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路。

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

5. 消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息。

  1. getSnapshotBeforeUpdate(prevProps, prevState) {
  2. return ...
  3. }
  1. componentDidUpdate(preProps, preState, valueFromSnapshot) {
  2. console.log("componentDidUpdate方法执行");
  3. console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
  4. }

为什么 componentWillUpdate 就非死不可呢?说到底,还是因为它“挡了 Fiber 的路”。

三、Fiber 架构简介

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

1. React16之前

在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。

同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回。这个漫长且不可打断的更新过程,将会带来用户体验层面的巨大风险:同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底完成。在这个过程中,浏览器没有办法处理任何渲染之外的事情,会进入一种无法处理用户交互的状态。因此若渲染时间稍微长一点,页面就会面临卡顿甚至卡死的风险。

2. React 16 引入的 Fiber 架构

Fiber 会将一个大的更新任务拆解为许多个小任务。每当执行完一个小任务时,渲染线程都会把主线程交回去,看看有没有优先级更高的工作要处理,确保不会出现其他任务被“饿死”的情况,进而避免同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是可以被打断的,这就是所谓的“异步渲染”。

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

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

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

四、废旧立新的思考

在 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)即使你没有开启异步,React 15 下也有不少人能把自己“玩死”。

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

五、结论

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

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

六、问与答

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

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

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

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