为什么 React 16 要更改组件的生命周期(下)

518 阅读8分钟

这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战

进化的生命周期方法:React 16 生命周期工作流详解

  这里推荐一个React生命周期大图,点击查看,先来看 React 16.3 的大图:

在这里插入图片描述

  这里之所以特意将版本号精确到了小数点后面一位,是因为在React 16.4之后,React 生命周期在之前版本的基础上又经历了一次微调。

  接下来,先把上面这张 React 16.3 生命周期大图中所涉及的内容讲清楚,然后再对 16.4 的改动进行介绍。

Mounting 阶段:组件的初始化渲染(挂载)

  为了凸显 16 和 15 两个版本生命周期之间的差异,将两个流程绘制到了同一张大图里,请看下面这张图:

在这里插入图片描述

消失的 componentWillMount,新增的 getDerivedStateFromProps

  getDerivedStateFromProps 不是 componentWillMount 的替代品,componentWillMount 的存在不仅“鸡肋”而且危险,因此它并不值得被“代替”,它就应该被废弃。

  getDerivedStateFromProps 这个 API,其设计的初衷不是试图替换掉 componentWillMount,而是试图替换掉 componentWillReceiveProps,因此它有且仅有一个用途:使用 props 来派生/更新 state

  值得一提的是,getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。React 16 以提供特定生命周期的形式,对这类诉求提供了更直接的支持。

认识 getDerivedStateFromProps

这个新生命周期方法的调用规则如下:

static getDerivedStateFromProps(props, state)

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

  1. getDerivedStateFromProps 是一个静态方法

    静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。

  2. 该方法可以接收两个参数:props state

    分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state

  3. getDerivedStateFromProps 需要一个对象格式的返回值

    如果没有指定这个返回值,那么大概率会被 React 警告一番

  getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。因此当确实不存在“使用 props 派生 state ”这个需求的时候,最好是直接省略掉这个生命周期方法的编写,否则一定记得给它 return 一个 null

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

Updating 阶段:组件的更新

  React 15 与 React 16.3 的更新流程对比如下图所示:

在这里插入图片描述

  React 16.4 对生命周期流程进行了“微调”,其实就调在了更新过程的getDerivedStateFromProps 这个生命周期上。先来看一张 React 16.4+ 的生命周期大图(出处仍然是Wojciech Maj 的 react-lifecycle-methods-diagram):

在这里插入图片描述

  React 16.4 的挂载和卸载流程都是与 React 16.3 保持一致的,差异在于更新流程上:

  • 在 React 16.4 中,任何因素触发的组件更新流程(包括由 this.setStateforceUpdate 触发的更新流程)都会触发 getDerivedStateFromProps

  • 而在 v 16.3 版本时,只有父组件的更新会触发该生命周期。

改变背后的第一个“Why”:为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps

  对于 getDerivedStateFromProps 这个 APIReact 官方曾经给出过这样的描述:

componentDidUpdate 一起,这个新的生命周期涵盖过时componentWillReceiveProps 的所有用例。

这句话里蕴含了下面两个关键信息:

  1. getDerivedStateFromProps 是作为一个试图代替 componentWillReceiveProps 的 API 而出现的;

  2. getDerivedStateFromProps不能完全和 componentWillReceiveProps 画等号,其特性决定了我们曾经在 componentWillReceiveProps 里面做的事情,不能够百分百迁移到getDerivedStateFromProps 里。

展开说说:

  1. 关于 getDerivedStateFromProps 是如何代替componentWillReceiveProps 的,在“挂载”环节已经讨论过:getDerivedStateFromProps 可以代替 componentWillReceiveProps 实现基于 props 派生 state
  2. 至于它为何不能完全和 componentWillReceiveProps 画等号,则是因为它过于“专注”了。这一点,单单从getDerivedStateFromProps 这个 API 名字上也能够略窥一二。原则上来说,它能做且只能做这一件事。

消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate

  先来看看 getSnapshotBeforeUpdate 是什么:

getSnapshotBeforeUpdate(prevProps, prevState) {

// ...

}

  这个方法和 getDerivedStateFromProps 颇有几分神似,它们都强调了“我需要一个返回值”这回事。区别在于 getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,我们可以同时获取到更新前的真实 DOM 和更新前后的 state&props 信息

  值得一提的是,这个生命周期的设计初衷,是为了“与 componentDidUpdate 一起,涵盖过时的 componentWillUpdate 的所有用例”。getSnapshotBeforeUpdate 要想发挥作用,离不开 componentDidUpdate 的配合

Unmounting 阶段:组件的卸载

在这里插入图片描述

卸载阶段的生命周期与 React 15 完全一致,只涉及 componentWillUnmount 这一个生命周期

透过现象看本质:React 16 缘何两次求变?

Fiber 架构简析

  Fiber 是 React 16 对 React 核心算法的一次重写,Fiber 会使原本同步的渲染过程变成异步的

  在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。下面这张图形象地展示了这个过程的特征:

在这里插入图片描述

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

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

在这里插入图片描述

换个角度看生命周期工作流

  Fiber 架构的重要特征就是可以被打断的异步渲染模式。但这个“打断”是有原则的,根据“能否被打断”这一标准,React 16 的生命周期被划分为了 render commit 两个阶段,而 commit 阶段又被细分为了 pre-commit commit。每个阶段所涵盖的生命周期如下图所示:

在这里插入图片描述

  来看下三个阶段各自有哪些特征:

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

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

  为什么这样设计呢?简单来说,由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,再狂的框架也不敢在用户眼皮子底下胡乱更改视图,所以这个过程必须用同步渲染来求稳。

细说生命周期“废旧立新”背后的思考

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

带着这个结论,再来看看 React 16 打算废弃的是哪些生命周期:

  • componentWillMount;
  • componentWillUpdate;
  • componentWillReceiveProps。

  这些生命周期的共性,就是它们都处于 render 阶段,都可能重复被执行,而且由于这些 API 常年被滥用,它们在重复执行的过程中都存在着不可小觑的风险。

  React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。在这个改造的过程中,React 团队精益求精,针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测