React
在 16.0
的版本当中,针对其之前的生命周期钩子进行了一定层度上的调整,所以在本章当中我们就来简单的汇总一下两个版本的生命周期有什么区别,以及为什么要进行这样的调整,更多关于 React
生命周期相关内容可以参考 官方文档
React 16.0 之前的生命周期
其实简单来说,React 16.0
之前的生命周期可以分为四个阶段
一、组件初始化阶段(Initialization)
constructor()
主要用来做一些组件的初始化工作,如定义 this.state
的初始内容,如下示例,Test
类继承了 React Component
这个基类,也是因为继承了这个基类,才能拥有 render()
,生命周期等方法可以使用,这也说明了为什么函数组件不能使用这些方法的原因
super(props)
用来调用基类的构造方法 constructor()
,也将父组件的 props
注入给子组件,供子组件读取,组件中的 props
只读不可变,state
可变,也可以根据 props
来设置 state
1 | class Test extends Comonent { |
当然,使用构造函数的方式是可选的,如果 Babel
设置了支持 类字段,则可以像下面这样初始化 state
1 | class Test extends Component { |
这种方法使用较为常见,我们仍然可以根据 props
设置 state
1 | class Test extends Component { |
但是,如果需要使用 ref
,可能仍需要构造函数
1 | class Test extends Component { |
我们需要构造函数调用 createRef
来创建对元素的引用,以便我们可以将它传递给某个组件,另外还可以在构造函数当中进行函数绑定,这也是可选的
二、组件的挂载阶段(Mounting)
此阶段分为 componentWillMount()
,render()
,componentDidMount()
三个时期
componentWillMount()
- 在组件挂载到
DOM
前调用,且只会被调用一次,在这个生命周期函数中调用this.setState()
不会引起组件重新渲染,也可以把写在这里面的内容提前到constructor()
中,所以项目中很少使用,所以这个生命周期钩子将『被废弃』
- 在组件挂载到
render()
- 根据组件的
props
和state
变化来执行渲染工作,render
是纯函数,所谓的纯函数指的是函数的返回结果只依赖于它的参数,函数执行的过程中没有副作用产生
- 根据组件的
componentDidMount()
- 组件挂载到
DOM
后调用,且只会调用一次,一般数据请求都会放到这个钩子当中来进行执行
- 组件挂载到
三、组件的更新阶段(Updation)
此阶段分为以下几个流程,这个阶段涉及到的内容较多,所以我们来稍微深入的了解一些
(即将过时)UNSAFE_componentWillReceiveProps()
shouldComponentUpdate()
(即将过时)UNSAFE_componentWillUpdate()
render()
componentDidUpdate()
首先要明确
React
组件更新机制,setState
引起的state
更新或者父组件重新render
引起的props
更新,更新后的state
和props
相对之前无论是否有变化,都将引起子组件的重新render
造成组件更新主要有两类情况,我们分类来进行阐述,其中『第一大类』是父组件重新 render
,它引起子组件重新 render
的情况又有两种,第一种情况就是直接使用父组件传递进来的 props
这种方式,父组件改变 props
后,子组件重新渲染,由于直接使用的 props
,所以我们不需要做什么就可以正常显示最新的 props
,每当父组件重新 render
导致的重新传递 props
,子组件将直接跟着重新渲染,无论 props
是否有变化,这种方式还可以通过 shouldComponentUpdate
方法优化
1 | class Child extends Component { |
第二种则是在 componentWillReceiveProps
方法中,将 props
转换成自己的 state
,这种方式,我们使用的是 state
,所以每当父组件每次重新传递 props
时,我们需要重新处理下,将 props
转换成自己的 state
,这里就用到了 componentWillReceiveProps
1 | class Child extends Component { |
根据官方描述『在该函数(componentWillReceiveProps
)中调用 this.setState()
将不会引起第二次渲染』,这是因为 componentWillReceiveProps
中判断 props
是否变化了,若变化了则 this.setState()
将引起 state
的变化,从而引起 render
,此时就没有必要再做第二次因重传 props
引起的 render
了,不然重复做一样的渲染
『第二大类』是组件本身调用 setState
无论 state
有没有变化,都会引起重新渲染,这种情况可以通过 shouldComponentUpdate
方法优化
1 | class Child extends Component { |
弄清楚了 React
组件的更新机制,我们回归正题,来详细看看之前提到过的各个更新阶段
UNSAFE_componentWillReceiveProps()(nextProps)
- 此方法只调用于
props
引起的组件更新过程中,响应props
变化之后进行更新的唯一方式,参数nextProps
是父组件传递给当前组件的新的props
- 但是父组件
render
方法的调用不能保证重传给当前组件props
是有变化的,所以在此方法中根据nextProps
和this.props
来查明重传的props
是否有改变,以及如果改变了要执行什么操作,比如根据新的props
调用this.setState
来触发当前组件的重新render
- 此方法只调用于
shouldComponentUpdate(nextProps, nextState)
- 此方法通过比较
nextProps
,nextState
以及当前组件的this.props
,this.state
,如果返回true
时当前组件将继续执行更新操作,返回false
则当前组件更新停止,借助此特性来减少组件的不必要渲染,优化组件的性能 - 这里也可以看出,就算
componentWillReceiveProps()
中执行了this.setState
更新了state
,但是在render
之前(比如shouldshouldComponentUpdate
和componentWillUpdate
)this.state
依然指向更新前的state
,不然nextState
以及当前组件的this.state
的对比就一直是true
了 - 如果
shouldComponentUpdate
返回false
那就一定不用重新渲染(rerender
)这个组件了,组件当中的组件元素(React Elements
)也不用去对比,但是如果shouldComponentUpdate
返回true
会进行组件的组件元素(React Elements
)对比,如果相同,则不用重新渲染(rerender
)这个组件,如果不同,会调用render
函数进行重新渲染(rerender
)
- 此方法通过比较
UNSAFE_componentWillUpdate()
- 此方法在调用
render
方法前执行,在这边可以执行一些组件的更新发生前的工作,一般比较少用
- 此方法在调用
render
render
方法触发组件的重新渲染
componentDidUpdate(prevProps, preState)
- 此方法在组件更新后被调用,可以操作更新的
DOM
,prevProps
和preState
这两个参数指向组件更新前的props
和state
- 此方法在组件更新后被调用,可以操作更新的
四、组件的卸载阶段(Unmounting)
componentWillUnmount
- 此阶段只有一个生命周期方法
componentWillUnmount
,此方法在组价被卸载时候调用,可以在这里执行一些清理工作,比如清除组件中使用的定时器,清除componentDidMount
中手动创建的DOM
元素等等,避免内存泄露
- 此阶段只有一个生命周期方法
React 16.0 之后的生命周期
变更缘由
原来的生命周期在 React 16
推出的 Fiber
之后就不合适了(关于 React Fiber
相关内容可以见 深入 React Fiber),因为如果要开启 async rendering
,那么在 render
函数之前的所有函数,都有可能执行多次,也就是
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
如果开启了 async rendering
而且又在以上这些生命周期方法当中去使用 Ajax
请求的话,那么 Ajax
将被无谓的多次调用,这明显不是我们期望的结果,而且在 componentWillMount
里面发起请求不管多快得到结果也赶不上首次 render
除了 shouldComponentUpdate
以外,其他在 render
函数之前的几个函数(componentWillMount/componentWillReceiveProps/componentWillUpdate
)都将被 getDerivedStateFromProps
替代,也就是说,使用一个静态函数 getDerivedStateFromProps
来取代即将被废弃的这几个生命周期函数,就是建议或者说是强制我们在 render
之前只做无副作用的操作
React 16
刚推出的时候增加了一个 componentDidCatch
生命周期函数,这只是一个增量式的修改,完全不影响原有的生命周期函数,但是到了 React 16.3
版本推出了大改动,引入了两个新的生命周期函数 getDerivedStateFromProps
和 getSnapshotBeforeUpdate
下面我们就来结合上面已经介绍过的一些生命周期,再加上新增的两个一起来梳理一下在变更之后的先后流程,更为详细的流程可以参考官方文档或是 React 生命周期 这个网站来进行查看
- 挂载阶段
constructor()
- 构造函数,最先被执行,一般会在构造函数里初始化
state
对象或者给自定义方法绑定this
,也是唯一可以直接修改state
的地方
- 构造函数,最先被执行,一般会在构造函数里初始化
static getDerivedStateFromProps(nextProps, prevState)
- 每次
render()
都会调用,通常当state
需要从props
初始化时使用,不过建议尽量不要使用,因为维护两者状态一致性会增加复杂度 - 典型的应用场景是表单控件获取默认值
- 每次
render()
- 纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的
DOM
、React
组件、Fragment
、Portals
、字符串、数字、布尔和null
等内容
- 纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的
componentDidMount()
- 组件挂载后(插入
DOM
树中)立即调用,并且只会执行一次,此时我们可以获取到DOM
节点并操作 - 典型的应用场景是获取(订阅)外部资源,但是记得在卸载阶段中取消订阅
- 组件挂载后(插入
- 更新阶段
static getDerivedStateFromProps()
- 此方法在更新个挂载阶段都可能会调用
shouldComponentUpdate(nextProps, nextState)
- 有两个参数
nextProps
和nextState
,表示新的属性和变化之后的state
,返回一个布尔值,true
表示会触发重新渲染,false
表示不会触发重新渲染,默认返回true
- 一般可以由
PureComponent
自动实现,通常利用此生命周期来优化React
程序性能
- 有两个参数
render()
- 更新阶段也会触发此生命周期
getSnapshotBeforeUpdate(prevProps, prevState)
- 这个方法在最近一次
render()
之前调用,一般不太常用,利用它可以获取render()
之前的DOM
状态(比如滚动位置等) - 有两个参数
prevProps
和prevState
,表示之前的属性和之前的state
,一个返回值,会作为第三个参数传给componentDidUpdate()
,如果你不想要返回值,可以返回null
- 通常与
componentDidUpdate()
搭配使用(但是需要注意getSnapshotBeforeUpdate()
和componentDidUpdate()
之间可能存在延迟)
- 这个方法在最近一次
componentDidUpdate(prevProps, prevState, snapshot)
- 该方法会在每次
UI
更新后会被立即调用(首次渲染不会执行此方法),有三个参数prevProps
,prevState
,snapshot
,表示之前的props
,state
和snapshot
,可以在componentDidUpdate()
中直接调用setState()
,但请注意它必须被包裹在一个条件语句里 - 如果
shouldComponentUpdate()
返回值为false
,则不会调用componentDidUpdate()
- 典型的应用场景是页面内容需要根据
props
变化重新获取数据
- 该方法会在每次
- 卸载阶段
componentWillUnmount()
- 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的
DOM
元素等垃圾清理工作
- 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的
- 错误处理
static getDerivedStateFromError()
- 渲染备用
UI
- 渲染备用
componentDidCatch()
- 主要用来打印错误信息,但是有一点需要注意,该方法仅适用于 渲染/生命周期 函数中的错误,如果应用程序在点击事件中抛出错误,它不会被捕获
下面我们主要来看看新增的两个方法
static getDerivedStateFromProps()
1 | static getDerivedStateFromProps(props, state) |
在 React 16.4
版本中,getDerivedStateFromProps()
方法无论是挂载(mounting
)还是更新(updating
),又或是其他什么引起的更新,全部都会被调用
这个生命周期就是为了替代 componentWillReceiveProps
存在的,所以在你需要使用 componentWillReceiveProps
的时候,就可以考虑使用 getDerivedStateFromProps
来进行替代了,两者的参数是不相同的,而 getDerivedStateFromProps
是一个静态函数,也就是这个函数不能通过 this
访问到 Class
的属性,也并不推荐直接访问属性,而是应该通过参数提供的 nextProps
以及 prevState
来进行判断,根据新传入的 props
来映射到 state
在 React 16.4
之后,getDerivedStateFromProps(nextProps, prevState)
方法会在组件创建和更新时的 render
方法之前被调用,值得注意的是,如果 props
传入的内容不影响你的 state
,那么你就返回一个 null
,这个返回值是必须的,所以尽量写在函数末尾
这里有一个需要注意的地方,即
getDerivedStateFromProps
前面要添加static
保留字,声明为静态方法,否则会被React
忽略掉,getDerivedStateFromProps
中的this
指向为undefined
,因为静态方法只能被构造函数调用,而不能被实例调用
1 | static getDerivedStateFromProps(nextProps, prevState) { |
其实简单来说,getDerivedStateFromProps
的作用就是为了让 props
能更新到组件内部 state
中,它的可能使用场景大概有两个
第一个,无条件的根据 props 来更新内部 state,也就是只要有传入 prop 值,就更新 state
我们来看下面这个例子,假设我们有个一个表格组件,它会根据传入的列表数据来更新视图
1 | class Table extends React.Component { |
上面的例子就是第一种使用场景,但是无条件从 prop
中更新 state
我们完全没必要使用这个生命周期,直接对 prop
值进行操作就好了,无需用 state
进行一个值的映射
第二个,只有 prop 值和 state 值不同时才更新 state 值
再看一个例子,这个例子是一个颜色选择器,这个组件能选择相应的颜色并显示,同时它能根据传入 prop
值显示颜色
1 | Class ColorPicker extends React.Component { |
现在我们可以使用这个颜色选择器来选择颜色,同时我们能传入一个颜色值并显示,但是这个组件存在一些问题,比如如果我们传入一个颜色值后,再使用组件内部的选择颜色方法,我们会发现颜色不会变化,一直是传入的颜色值
这也是使用这个生命周期的一个比较常见的问题,为什么会出现这样的问题呢?我们在之前提到过,在 React 16.4^
的版本中,setState
和 forceUpdate
也会触发这个生命周期,所以内部 state
变化后,又会走 getDerivedStateFromProps
方法,并把 state
值更新为传入的 prop
,所以下面我们来稍微的调整一下
1 | Class ColorPicker extends React.Component { |
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate()
被调用于 render
之后,可以读取但无法使用 DOM
的时候,它可以让我们的组件在可能更改之前从 DOM
捕获一些信息(例如滚动位置),此生命周期返回的任何值都将作为参数传递给 componentDidUpdate
,但是一般使用不是很多,了解即可
1 | class ScrollingList extends React.Component { |