前言

如果说 JSX 是学习 React 框架必须要理解的一个概念,那么“生命周期”则是紧随其后的第二个须要学习的内容。尽管当初最新的 React 版本都举荐应用函数组件联合 hooks 的形式来组织利用,而且在函数组件中淡化了对生命周期的介绍,然而对于类组件生命周期的学习了解可能帮忙咱们以追根溯源的形式,更全面的建设对 React 框架的观感,同时从底层意识 React 中两个重要的阶段:render 与 commit ,帮忙咱们编写正确且高效的代码。

本次分享会从上面的几个问题登程:

  • 生命周期到底是什么?
  • 类组件生命周期函数有哪些,都在什么场景下应用?
  • 生命周期函数演变的起因是什么?
  • Hooks 与 生命周期函数的对应关系是什么?

React 中的生命周期到底是什么

生命周期(lifecycle)的概念在各个领域中都宽泛存在,狭义来说生命周期泛指自然界和人类社会中各种客观事物的阶段性变动及其法则,在 React 框架中则用来形容组件挂载(创立)、更新(存在)、卸载(销毁)三个阶段。

基本上咱们每个新同学都会被要求通读 React 官网的材料,通过这样的形式建设起对 React 框架大抵的全像。在此过程中应该能察觉到 React 始终在重复的提及两个关键词:虚构 DOM 与 组件

在后面意识 JSX 的过程中,咱们晓得虚构 DOM 的实质就是通过编译 JSX 失去的一个以 JavaScript 对象模式存在的 DOM 构造形容。在组件初始化阶段,会通过生命周期办法 render 生成虚构 DOM节点,而后通过调用 ReactDOM.render 办法,实现虚构 DOM 节点到实在 DOM 节点的转换。在组件更新阶段,会再次调用 render 办法生成新的虚构 DOM 节点,而后借助Diffing 算法比对两次虚构 DOM 节点的差别,从而找出变动的局部实现最小化的 DOM 更新。所以也能够说虚构 DOM 是 React 外围算法 Diffing 的基石。

组件化是一种优良的软件设计思维,这在 React 框架中失去了很好的体现。React 我的项目中基本上所有的根底单元就是组件,通过组合各种组件构建利用。每个组件既是关闭又是凋谢。关闭体现在组件外部有本人的一套渲染逻辑(state),在没有数据流交互的状况下,组件与组件之间互不烦扰。凋谢则体现在组件间的通信上,基于单向数据流的准则进行通信(props),而数据通信又会对渲染后果造成影响,通过数据这个桥梁,组件之间彼此凋谢,相互影响。关闭与凋谢的个性使得 React 组件具备高度的可维护性可重用性

虚构 DOM 节点的生成依赖于 render 办法,而组件化中的渲染工作流(组件数据扭转到组件理论更新的过程)也离不开 render 办法,render 办法的重要性可见一斑。总结来说,生命周期就是虚构 DOM 实现的基石,同时也是 React 组件化软件思维的间接体现,通过生命周期函数反对组件具备关闭与凋谢的个性。

组件生命周期概览

很多文章及教程介绍类组件生命函数,都是以最新的 React 来进行解说,这样的益处是能够让初学者最快捷的把握最新的 API,而后可能马上投入到开发中。然而与此同时也会导致初学者停留在“能用”的阶段,离“会用”还有一段距离。本文咱们从最开始的 React v15 来开展,而后进入到 v16.3 与 v16.4,比照几个版本之间的差别,这样的形式不是为了让大家多记几个 API,而是找到差别并提出问题,不便咱们在下一节搞清楚为什么须要对生命周期函数进行改良。这样的形式与间接学 API 相比,无非就是多花了几分钟进行思考,带来的收益是帮忙咱们从“能用”往“会用”迈进。

React v15 中的生命周期函数次要如下图所示:

constructor(props)componentWillMount()componentWillReceiveProps(nextProps)shouldComponentUpdate(nextProps, nextState)componentDidMount()componentWillUpdate(nextProps, nextState)componentDidUpdate(prevProps, prevState, snapshot)render()componentWillUnmount()// 挂载: constructor -> componentWillMount -> render -> componentDidMount// 更新(父组件触发): componentWillReceiveProps -> shouldComponentUpdate(true) -> componentWillUpdate -> render -> componentDidUpdate// 更新(组件外部触发): shouldComponentUpdate(true) -> componentWillUpdate -> render -> componentDidUpdate// 卸载: componentWillUnmount

在 React 晚期,还能够应用 React.createClass() 办法创立组件,在此状况下还有 getDefaultProps与 getInitState 两个生命周期函数。ES6 遍及后,这种创立组件的办法就不被举荐应用了,所以此处不做过多的补充。

对于生命周期函数,这里有几点须要特地留神:

  • shouldComponentUpdate 办法能够指定一个布尔类型的返回值,如果该办法返回值为 false,则能够跳过更新,不执行后续的生命周期办法
  • componentWillReceiveProps 的触发不是因为传递 props 变动,而是父组件只有被 re-render(重渲染),那么子组件的 componentWillReceiveProps 就会被执行
  • componentDidUpdate 生命周期函数的两个参数区别于其余的生命周期函数,传入的不是nextPropsnextState,而是prevProps与 prevState,以后的propsstate须要从this对象上获取
  • componentWillUnmount 会在组件被销毁时执行,个别状况组件有两种状况下会被销毁:一个是在父组件中被移除,二是组件被设置了 key 值,父组件在 render 的过程发现 key 与上一次的不统一,那么这个组件也会被销毁,而后被从新初始化,从新设置 key

这里咱们就不对每一个生命周期函数开展阐明了,更具体的能够通过点击下方浏览原文,查看 React 官网 自行理解,也能够通过这个例子增强了解:React15 Lifecycle Demo

PS:如果关上例子无奈失常预览,报错:Target container is not a DOM element,不必惊恐,这是 codesandbox 的问题,此时能够关上任意一个左侧的源代码文件,而后保留一下,预览区域即可恢复正常

React v16 开始,对生命周期函数做了一些更改,且分为两个版本:v16.3 及之前的版本,与 v16.4 及之后的版本

React 生命周期查看在线地址:https://projects.wojtekmaj.pl...

constructor(props)static getDerivedStateFromProps(props, state)shouldComponentUpdate(nextProps, nextState)getSnapshotBeforeUpdate(prevProps, prevState)componentDidMount()componentDidUpdate(prevProps, prevState, snapshot)render()componentWillUnmount()// 挂载: constructor -> getDerivedStateFromProps(null) -> render -> componentDidMount// 更新(父组件触发): getDerivedStateFromProps(null) -> shouldComponentUpdate(true) -> getSnapshotBeforeUpdate -> render -> componentDidUpdate// 更新(组件外部触发): getDerivedStateFromProps(null) -> shouldComponentUpdate(true) -> getSnapshotBeforeUpdate -> render -> componentDidUpdate// 卸载: componentWillUnmount

React v16 与 v15 相比变动还是挺大的(16.3-4之间的变动较小),次要集中在以下几个方面:

  • componentWillMount 与 componentWillUpdate 及 componentWillReceiveProps
  • 新增了getDerivedStateFromProps 与 getSnapshotBeforeUpdate
  • 新增了getDerivedStateFromError 与 componentDidCatch 谬误处理函数

尽管新增的两个办法与废除的办法它们在触发程序上大致相同,然而不能简略的认为是新办法代替旧办法。

getDerivedStateFromProps 办法的目标不是为了替换 componentWillMount,而是为了替换 componentWillReceiveProps。该办法是一个静态方法(static),静态方法不依赖于组件的实例而存在,所以无奈在办法外部读取 this 对象,而且它应该返回一个新的对象,或者一个 null 值,它存在的目标有且仅有一个:应用 props 来派生/更新 state,所有不是以此为指标的应用形式原则上来说都是谬误的。

getDerivedStateFromProps 不仅是在更新阶段会被调用,在挂载阶段也会被调用,这是因为派生 state 的诉求不仅仅在更新时存在,在初始化 state 时也会有需要。通过该办法派生 state 不会引起 render 函数反复执行。以此来看,该办法的呈现不是简略的替换逻辑,而是有着承载简化代码的冀望。

getSnapshotBeforeUpdate 办法提供了一个机会读取以后 DOM 的一些信息,并把返回的值赋值给 componentDidUpdate 办法的 snapshot 参数,次要用来解决 UI 显示,比方某些区域的滚动地位信息等。

能够通过这个例子增强对新生命周期函数的了解:React16 Lifecycle Demo。

生命周期函数演变的起因

咱们大抵晓得废除 componentWillMount 办法的起因,因为这个办法切实是没什么用。然而为什么要用getDerivedStateFromProps代替 componentWillReceiveProps 呢,除了简化派生 state 的代码,是否还有别的起因?

原来的 componentWillReceiveProps 办法仅仅在更新阶段才会被调用,而且在此函数中调用 setState 办法更新 state 会引起额定的 re-render,如果处理不当可能会造成大量无用的 re-render。getDerivedStateFromProps 相较于 componentWillReceiveProps 来说不是做加法,而是做减法,是 React 在推广只用 getDerivedStateFromProps 来实现 props 到 state 的映射这一最佳实际,确保生命周期函数的行为更加可控可预测,从本源上帮忙开发者防止不合理的编程形式,同时也是在为新的 Fiber 架构 铺路。

getSnapshotBeforeUpdate 配合 componentDidUpdate 办法能够涵盖所有 componentWillUpdate应用场景,那废除 componentWillUpdate 的起因就是换另外一种形式吗?其实根本原因还是在于 componentWillUpdate 办法是 Fiber 架构落地的一块绊脚石,不得不废除掉。

Fiber 是 React v16 对 React 外围算法的一次重写,简略的了解就是 Fiber 会使本来同步的渲染过程变成增量渲染模式

在 React v16 之前,每触发一次组件的更新,都会构建一棵新的虚构 DOM 树,通过与上一次的虚构 DOM 树进行 Diff 比拟,实现对实在 DOM 的定向更新。这一整个过程是递归进行的(想想 React 利用的组织模式),而同步渲染的递归调用栈档次十分深(代码写得不好的状况下非常容易导致栈溢出),只有最底层的调用返回,整个渲染过程才会逐层返回。这个漫长的更新过程是不可中断的,同步渲染一旦开始,主线程(JavaScript 解析与执行)会始终被占用,直到递归彻底实现,在此期间浏览器没有方法解决任何渲染之外的事件(比如说响应用户事件)。这个问题对于大型的 React 利用来说是没方法承受的。

在 React v16 中的 Fiber 架构正是为了解决这个问题而提出的:Fiber 会将一个大的更新工作拆解为许多个小工作。每一个小工作执行实现后,渲染过程会把主线程交回去(开释),看看有没有其它优先级更高的工作(用户事件响应等)须要解决,如果有就执行高优先级工作,如果没有就继续执行其余的小工作。通过这样的形式,防止主线程被长时间的独占,从而防止利用卡顿的问题。这种能够被打断的渲染过程就是所谓的异步渲染。

Fiber 带来了两个重要的个性:工作拆解 与 渲染过程可打断。对于可打断并不是说任意环节都能打断从新执行,可打断的机会也是有所辨别的。依据是否被打断这一规范,React v16 的生命周期被划分为了 render 和 commit两个阶段(commit 又被细分为 pre-commit 和 commit)。

  • render 阶段:污浊且没有副作用,能够被 React 暂停,终止或重新启动
  • pre-commit 阶段:能够读取 DOM
  • commit 阶段:能够应用 DOM,运行副作用,安顿更新

总体来说就是,render 阶段在执行过程中容许被打断,commit 阶段则总是同步执行。之所以确定这样的规范也是有深刻思考的,在 render 阶段的所有操作个别都是不可见的,所以被反复打断与从新执行,对用户来说是无感知的,在 commit 阶段会波及到实在 DOM 的操作,如果该阶段也被重复打断从新执行,会导致 UI 界面屡次更改渲染,这是相对要防止的问题。

在理解了 Fiber 架构的执行机制之后,再回过头去看一下被废除的生命周期函数:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

这些生命周期的共性就是它们都处于 render 阶段,都可能被暂停,终止和从新执行。而如果开发者在这些函数中运行了副作用(或者操作 DOM),那么副作用函数就有可能会被多次重复执行,会带来意料之外的重大 bug。

最初咱们梳理一下 React 生命周期函数演变背地的逻辑:

  • 为 Fiber 架构落地革除阻碍,引入增量渲染的机制解决同步渲染引起的利用卡顿危险
  • 以废除改良 API 的形式防止开发者滥用生命周期函数,推广强制性的最佳实际(每一个值有且仅有一个明确的起源)
    对于生命周期函数滥用能够参考:**你可能不须要应用派生state 
    *https://zh-hans.reactjs.org/b...*

Hooks 与 生命周期函数

生命周期函数只存在于类组件,对于没有 Hooks 之前的函数组件而言,没有组件生命周期的概念(函数组件没有 render 之外的过程),然而有了 Hooks 之后,问题就变得有些简单了。

Hooks 可能让函数组件领有应用与治理 state 的能力,也就演化出了函数组件生命周期的概念(render 之外新增了其余过程),波及到的 Hook 次要有几个:useState、useMemo、useEffect。

更全面的 Hooks 介绍能够复制查看:https://zh-hans.reactjs.org/d...

生命周期办法与 Hook 的对应:https://zh-hans.reactjs.org/d...

整体来说,大部分生命周期都能够利用 Hook 来模仿实现,而一些难以模仿的,往往也是 React 不举荐的反模式。

至于为什么设计 Hook,为什么要赋予函数组件应用与治理 state 的能力,React 官网也在 Hook 介绍 做了深刻而具体的介绍,总结下来有以下几个点:

  • 便于拆散与复用组件的状态逻辑(Mixin,高阶组件,渲染回调模式等)
  • 简单组件变得难以了解(状态与副作用越来越多,生命周期函数滥用)
  • 类组件中难以了解的 this 指向(bind 语法)
  • 类组件难以被进一步优化(组件预编译,不能很好被压缩,热重载不稳固)

总结

至此,文章后面提出的几个问题都失去了比拟深刻的解答,而且在寻求答案的过程中,也对 React 框架有了更平面的意识。心愿可能通过本文能帮忙大家从“能用”往“会用”的方向迈进几步,也欢送大家把本人的心得或疑难写在评论区,不便咱们进一步沟通探讨。

参考资料

  • State&生命周期:
    https://zh-hans.reactjs.org/d...
  • 组件 & Props:
    https://zh-hans.reactjs.org/d...
  • React.Component:
    https://zh-hans.reactjs.org/d...
  • Hook 简介:
    https://zh-hans.reactjs.org/d...
  • Hook 概览:
    https://zh-hans.reactjs.org/d...
  • Hook API 索引:
    https://zh-hans.reactjs.org/d...
  • Render and Commit(Beta):
    https://beta.reactjs.org/lear...