前言
组件在初始化时,会通过调用生命周期中的 render 方法,生成虚拟 DOM,然后再通过调用 ReactDOM.render 方法,实现虚拟 DOM 到真实 DOM 的转换。当组件更新时,会再次通过调用 render 方法生成新的虚拟 DOM,然后借助 diff算法定位出两次虚拟 DOM 的差异,从而针对发生变化的真实 DOM 作定向更新。
旧生命周期 React15
constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()
组件挂载阶段
componentWillMount、componentDidMount 方法同样只会在挂载阶段被调用一次。其中 componentWillMount 会在执行 render 方法前被触发。render 在执行过程中并不会去操作真实 DOM(也就是说不会渲染),它的职能是把需要渲染的内容返回出来。真实 DOM 的渲染工作,在挂载阶段是由 ReactDOM.render 来承接的。componentDidMount 方法在渲染结束后被触发,此时因为真实 DOM 已经挂载到了页面上,我们可以在这个生命周期里执行真实 DOM 相关的操作。此外,类似于异步请求、数据初始化这样的操作也大可以放在这个生命周期来做(侧面印证了 componentWillMount 没啥用)
组件更新阶段
组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新。
componentReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的: 父组件传递给子组件的props发生变化,子组件会触发这个钩子函数;父组件传递给子组件的props没有发生变化,单纯更新了一下,也会触发这个钩子函数。
componentWillUpdate 会在 render 前被触发,它和 componentWillMount 类似,允许你在里面做一些不涉及真实 DOM 操作的准备工作;而 componentDidUpdate 则在组件更新完毕后被触发,和 componentDidMount 类似,这个生命周期也经常被用来处理 DOM 操作。此外,常常将 componentDidUpdate 的执行作为子组件更新完毕的标志通知到父组件。
render 方法由于伴随着对虚拟 DOM 的构建和对比,过程可以说相当耗时。而在 React 当中,很多时候我们会不经意间就频繁地调用了 render。为了避免不必要的 render 操作带来的性能开销,React 为我们提供了 shouldComponentUpdate 这个口子。React 组件会根据 shouldComponentUpdate 的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)。shouldComponentUpdate 的默认值为 true,也就是说“无条件 re-render”。在实际的开发中,我们往往通过手动往 shouldComponentUpdate 中填充判定逻辑,或者直接在项目中引入 PureComponent 等最佳实践,来实现“有条件的 re-render”,现在使用PureComponent组件可以避免无效渲染。
组件卸载阶段
componentWillUnmount()钩子在组件卸载阶段执行。
- 组件在父组件中被移除了:这种情况相对比较直观,对应的就是我们上图描述的这个过程。
- 组件中设置了 key 属性,父组件在 render 的过程中,发现 key 值和上一次不一致,那么这个组件就会被干掉。
新生命周期 React16
React 16 对 render 方法也进行了一些改进,React 16 之前,render方法必须返回单个元素,而 React 16 允许我们返回元素数组和字符串。
组件挂载阶段
消失的 componentWillMount,新增 getDerivedStateFromProps,getDerivedStateFromProps 并不是 componentWillMount 的替代品,因为 componentWillMount本身就是鸡肋的存在,不应该被代替,应该被废弃。
而 getDerivedStateFromProps 这个 API,直译过来就是“从 Props 里派生 State”,其设计的初衷不是试图替换掉 componentWillMount,而是试图替换掉 componentWillReceiveProps,因此它有且仅有一个用途:使用 props 来派生/更新 state。
getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。
static getDerivedStateFromProps(props, state)
- getDerivedStateFromProps 是一个静态方法。静态方法不依赖组件实例而存在,因此你在这个方法内部是访问不到 this 的。假如用了,必定报错。
- 第二个重点,该方法可以接收两个参数:props 和 state,它们分别代表当前组件接收到的来自父组件的 props 和当前组件自身的 state。
- 第三个重点,getDerivedStateFromProps 需要一个对象格式的返回值。如果没有指定这个返回值,那么大概率会被 React 警告一番。getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。因此当你确实不存在“使用 props 派生 state ”这个需求的时候,最好是直接省略掉这个生命周期方法的编写,否则一定记得给它 return 一个 null。
注意,getDerivedStateFromProps 方法对 state 的更新动作并非“覆盖”式的更新,而是针对某个属性的定向更新
组件更新阶段
对于 getDerivedStateFromProps 这个 API,React 官方曾经给出过这样的描述:
与 componentDidUpdate 一起,这个新的生命周期涵盖过时componentWillReceiveProps 的所有用例。
- getDerivedStateFromProps 是作为一个试图代替 componentWillReceiveProps 的 API 而出现的。
- getDerivedStateFromProps不能完全和 componentWillReceiveProps 画等号,其特性决定了我们曾经在 componentWillReceiveProps 里面做的事情,不能够百分百迁移到getDerivedStateFromProps 里。
在“挂载”环节 getDerivedStateFromProps 可以代替 componentWillReceiveProps 实现基于 props 派生 state,也就是初始化。从 getDerivedStateFromProps 直接被定义为 static 防范, static 静态方法内部拿不到组件实例的 this,这就导致无法在 getDerivedStateFromProps 里面做任何类似于 this.fetch()、不合理的 this.setState(会导致死循环的那种)这类可能会产生副作用的操作。
getSnapshotBeforeUpdate(prevProps, prevState) {
// ...
}
这个方法和 getDerivedStateFromProps 颇有几分神似,它们都强调了“我需要一个返回值”这回事。区别在于 getSnapshotBeforeUpdate 的返回值会作为第三个参数给到 componentDidUpdate。它的执行时机是在 render 方法之后,真实 DOM 更新之前。在这个阶段里,可以同时获取到更新前的真实 DOM 和更新前后的 state & props 信息。
值得一提的是,这个生命周期的设计初衷,是为了“与 componentDidUpdate 一起,涵盖过时的 componentWillUpdate 的所有用例”(引用自 React 官网)。getSnapshotBeforeUpdate 要想发挥作用,离不开 componentDidUpdate 的配合。
组件卸载阶段
卸载阶段的生命周期与 React 15 完全一致,只涉及 componentWillUnmount 这一个生命周期。
生命周期为何变化
Fiber 架构是 React16 对 React 核心算法的一次重写:使原本同步的渲染过程变成异步的
在 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 阶段则总是同步执行的。
在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。
React 16 打算废弃的是哪些生命周期:
- componentWillMount;
- componentWillUpdate;
- componentWillReceiveProps。
这些生命周期的共性,就是它们都处于 render 阶段,都可能重复被执行,它们在重复执行的过程中都存在着不可小觑的风险。
比如在 componentWillMount 里发起异步请求。以为这样做就可以让异步请求回来得“早一点”,从而避免首次渲染白屏。但是异步请求再怎么快也快不过(React 15 下)同步的生命周期。componentWillMount 结束后,render 会迅速地被触发,所以说首次渲染依然会在数据返回之前执行。这样做不仅没有达到你预想的目的,还会导致服务端渲染场景下的冗余请求等额外问题,得不偿失。
总的来说,React 16 改造生命周期的主要动机是为了配合 Fiber 架构带来的异步渲染机制。在这个改造的过程中,React 团队精益求精,针对生命周期中长期被滥用的部分推行了具有强制性的最佳实践。这一系列的工作做下来,首先是确保了 Fiber 机制下数据和视图的安全性,同时也确保了生命周期方法的行为更加纯粹、可控、可预测。
特此感谢修言老师!