一、概念
生命周期指 React 组件从装载至卸载的全过程,这个过程内置多个函数供开发者在组件的不同阶段执行需要的逻辑。
状态组件主要通过 3 个生命周期阶段来管理,分别是 挂载阶段(MOUNTING),更新阶段(UPDATING)和卸载阶段(UNMOUNT)。
从纵向划分,可以划分为 Render 阶段和 Commit 阶段
Render 阶段:纯净且不包含副作用,可能会被 React 暂停、中止或重新启动
Commit 阶段:可以使用 DOM,运行副作用,安排更新
二、挂载阶段触发生命周期
1、constructor
构造函数(这个并不是react的生命周期)
语法
class A extends React.Component {
/**
*
* @param {*} props 继承 React.Component 的属性方法,它是不可变的 read-only
* @param {*} context 全局上下文。
* @param {*} updater 包含一些更新方法的对象
* 比如this.setState 最终调用的是 this.updater.enqueueSetState
*/
constructor(props, context, updater) {
super(props)
}
}
触发时机
在组件初始化的时候触发一次。
使用场景
1、设置初始化状态
因为组件的生命周期中任何函数都可能要访问 State,那么整个周期中第一个被调用的构造函数便是初始化 state 最理想的地方;
2、绑定成员函数上下文引用
因为在 ES6 语法下,类的每个成员函数在执行时的 this 并不是和类实例自动绑定的;
而在构造函数中 this 就是当前组件实例,所以,为了方便将来调用,可以将这个实例的特定函数绑定 this 为当前类实例;
建议定义函数方法时直接使用箭头函数,就无须在构造函数中进行函数的 bind 操作。
2、static getDerivedStateFromProps
语法
/**
*
* @param {*} nextProps 当前的props
* @param {*} prevState 修改前的state
* 该生命周期函数必须有返回值,
* 它需要返回一个对象来更新 State,
*/
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.num) {
return {
...prevState,
num: nextProps.num * 2
}
}
return prevState;
}
触发时机
该函数会在组件化实例化后和重新渲染前调用(生成 VirtualDOM 之后,实际 DOM 挂载之前),意味着无论是父组件的更新、props 的变化或通过 setState 更新组件内部的 State,它都会被调用。
注意
在组件装载和更新阶段都会触发。
如果父组件导致了组件的重新渲染,即使属性没有更新,这一方法也会被触发;
如果你只想处理 props 的前后变化,你需要将上一个 props 值存到 state 里作为镜像;
该生命周期函数是一个静态函数,所以函数体内无法访问指向当前组件实例的指针 this;
使用场景
该生命周期函数被设计成静态方法的目的是为了保持该方法的纯粹。通过根据父组件输入的 props 按需更新 state,这种 state 叫做衍生 state,返回的对象就是要增量更新的 state,除此之外不应该在里面执行任何操作。
通过设计成静态方法,能够起到限制开发者无法访问 this 也就是实例的作用,这样就不能在里面调用实例方法或者 setState 以破坏该生命周期函数的功能。
这个生命周期函数也经历了一些波折,原本它是被设计成 初始化、父组件更新 和 接收到 Props 才会触发,现在只要渲染就会触发,也就是 初始化 和 更新阶段 都会触发
import React from "react";
export default class LearnLife extends React.Component {
state = {
num: 1,
};
/**
*
* @param {*} nextProps 当前的props
* @param {*} prevState 修改前的state
* 该生命周期函数必须有返回值,
* 它需要返回一个对象来更新 State,
* 或者返回 null 表达不更新
*/
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.num) {
return {
...prevState,
num: nextProps.num * 2
}
}
return nextProps;
}
render() {
return (
<div>
<div>父组件2倍的num -- {this.state.num}</div>
<div>父组件传过来的值 -- {this.props.num}</div>
</div>
);
}
}
3、UNSAFE_componentWillMount(componentWillMount)
此生命周期函数将在 React v17 正式废弃。
触发时机
在构造函数和装载组件(将 DOM 树渲染到浏览器中)之间触发。装载组件后将执行 render 渲染函数。因此在此生命周期函数里使用 setState 同步设置组件内部状态 state 将不会触发重新渲染。
注意
避免在该方法中引入任何的副作用(Effects)或订阅(Subscription)。对于这些使用场景,建议提前到构造函数中
4、render
渲染函数(这个并不是react的生命周期)
概念
它是一个仅仅用于渲染的纯函数,返回值完全取决于 state 和 props,不能在函数中任何修改 state、props、请求数据等具有副作用的操作,不能读写 DOM 信息,也不能和浏览器进行交互(例如 setTimeout)。如果需要和浏览器交互,在 componentDidMount() 中或者其它生命周期方法完成相关事务。
此渲染函数并不做实际的渲染动作(渲染到 DOM 树),它返回的只是一个 JSX 的描述对象(及组件实例),何时进行真正的渲染是有 React 库决定的。而 React 肯定是要把所有组件返回的结果综合起来,才能知道如何产生真实的 DOM 树。也就是说,只有 React 库调用所有组件的渲染函数之后,才有可能完成 DOM 装载,这时候才会依次调用 componentDidMount 函数作为装载的收尾。
注意
请勿在此函数中使用 setState 方法;
请勿在此函数中修改 props、state 以及数据请求等具有副作用的操作
5、componentDidMount
语法
class Life extends React.component {
componentDidMount () {
}
}
触发时机
组件完全挂载到网页上后触发。
使用场景
发送网络请求;任何依赖于 DOM 的初始化操作;添加事件监听;如果使用了 Redux 之类的数据管理工具,也能触发 action 处理数据变化逻辑
为什么在这个生命周期里面发送网络请求
之所以在 componentDidMount 中而不是在构造函数中进行数据请求的原因在于:如果数据加载完毕后,即 state 已经有值了,但是组件还没有渲染出来,会报错。但是这里有一些把数据拉取提前到 constructor 函数的思路:在 contructor 函数中,通过 Promise 来进行数据的请求,并且绑定到当前实例对象上,然后在 componentDidMount 中执行 Promise 把数据更新到 state 上
三、更新阶段触发的生命周期
1、UNSAFE_componentWillReceiveProps(componentWillReceiveProps)
此生命周期函数将在 React v17 正式废弃。推荐使用 static getDerivedStateFromProps() 代替
语法
/*
nextProps 当前的props
*/
componentWillReceiveProps(nextProps) {
}
触发时机
当父组件的渲染函数被调用,在渲染函数中被渲染的子组件就会经历更新阶段,不管父组件传给子组件的 props 有没有改变,都会触发该生命周期函数。当组件内部调用 setState 更新内部状态 state 时触发更新阶段不会触发该函数
2、shouldComponentUpdate
语法
在一个更新生命周期中,组件及其子组件将根据该方法返回的布尔值来决定是否继续这次更新过程(重新渲染)。这样你可以在必要的时候阻止组件的渲染生命周期(Render Lifecycle)方法,避免不必要的渲染
/**
*
* @param {*} nextProps 当前的props
* @param {*} nextState 当前的state
* @returns
根据逻辑判断返回 true 表示继续进行组件渲染,否则将停止组件渲染过程。
默认返回 true,也就是说,只要组件触发了更新,组件就一定会更新
*/
shouldComponentUpdate (nextProps, nextState) {
return true
}
触发时机
每次组件因为 state 和 props 变化而更新时,在重新渲染前该生命周期函数都会触发,让 React 知道当前 state 或 props 的改变是否影响组件的输出(渲染)。
使用场景
如果性能是个瓶颈,尤其是有几十个甚至上百个组件的时候,使用 shouldComponentUpdate 可以优化渲染效率,提升应用的性能;
使用 React.PureComponent 组件基类能自动实现一个 shouldComponentUpdate 生命周期钩子,可以默认为组件更新校验,但是只会对更新数据进行浅层对照;
通常用于条件渲染,优化渲染的性能
例子
实现点击按钮num+1 当num为偶数据的时候视图不渲染
import React from "react";
export default class ShouldComponentUpdate extends React.Component {
state = {
num: 1,
};
/**
*
* @param {*} nextProps 当前的props
* @param {*} nextState 当前的state
* @returns return false 不渲染 true 渲染
*/
shouldComponentUpdate(nextProps, nextState) {
/**
* 偶数的时候不渲染
*/
return nextState.num % 2 !== 0;
}
add() {
this.setState({
num: this.state.num + 1,
});
}
render() {
return (
<div>
<div>{this.state.num}</div>
<button onClick={() => this.add()}>+1</button>
</div>
);
}
}
3、UNSAFE_componentWillUpdate(componentWillUpdate)
此生命周期函数将在 React v17 正式废弃。建议使用componentWillRreceiveProps代替
触发时机
这个方法是一个更新生命周期中重新渲染执行之前的最后一个方法。你已经拥有了下一个属性和状态,他们可以在这个方法中任由你处置。你可以利用这个方法在渲染之前进行最后的准备
注意
此钩子函数在初始化渲染的时候不会被触发;
请勿在此函数中使用 setState 方法,会导致循环调用。
4、render
渲染函数。与上文所提及的 render 生命周期函数一致,用于输出 JSX 并经过 React 处理后渲染至浏览器
5、getSnapshotBeforeUpdate
语法
/**
*
* @param {*} prevProps 更新前的props
* @param {*} prevState 更新前的state
* @returns 返回值可以在componentDidUpdate中接收
*/
getSnapshotBeforeUpdate (prevProps, prevState) {
return null
}
触发时机
该生命周期函数会在组件即将挂载时触发,它的触发在 render 渲染函数之后。由此可见,render 函数并没有完成挂载操作,而是进行构建抽象 UI(也就是 Virtual DOM)的工作。该生命周期函数执行完毕后就会立即触发 componentDidUpdate 生命周期钩子。
使用场景
该生命周期函数能让你捕获某些从 DOM 中才能获取的(可能会变更的)信息(例如,元素重新渲染后页面各种定位位置的变更等)。比如网页滚动位置,不需要它持久化,只需要在组件更新以后能够恢复原来的位置即可
注意
该生命周期函数返回的值将作为第三个参数传递给 componentDidUpdate,我们可以利用这个通道保存一些不需要持久化的状态,用完即可舍弃。
(这个生命周期不是经常需要的,但可以用于在恢复期间手动保存滚动位置的情况。)
该函数的出现是为了 React 17 的异步渲染而准备的
6、componentDidUpdate
语法
/**
*
* @param {*} prevProps 更新前的props
* @param {*} prevState 更新前的state
* @param {*} snapshot getSnapshotBeforeUpdate传过来的
*/
componentDidUpdate (prevProps, prevState, snapshot) {
}
触发时机
组件每次重新渲染后触发,相当于首次渲染(初始化)之后触发 componentDidMount
注意
在该生命周期中使用 setState 时,必须加 if 条件判断,通过判断 prevProps、prevState 和 this.state 之间的数据变化,来判断是否执行相关的 state 变更逻辑,这使得尽管在 componentDidUpdate 中调用了 setState 进行再更新,但是直至条件不成立,就不会造成程序死循环。
此生命周期函数不会在初始化渲染的时候触发
总结
相比装载阶段的生命周期函数,更新阶段的生命周期函数使用的相对来说要少一些。常用的是 getDerivedStateFromProps、shouldComponentUpdate,前者经常用于根据新 props 的数据去设置组件的 State,而后者则是常用于优化,避免不必要的渲染
四、卸载阶段
1、componentWillUnmount
语法
componentWillUnmount () {
}
触发时机
在组件卸载和销毁之前触发。可以利用这个生命周期方法去执行任何清理任务
使用场景
用于注销事件监听器;取消网络请求;取消定时器;解绑 DOM 事件。
注意
在该方法中调用 setState 不会触发 render,因为所有的更新队列,更新状态都被重置为 null
五、为什么react17后删除了那些生命周期
react 打算在17版本推出新的 Async Rendering,提出一种可被打断的生命周期,而可以被打断的阶段正是实际 dom 挂载之前的虚拟 dom 构建阶段,也就是要被去掉的三个生命周期。本身这三个生命周期所表达的含义是没有问题的,但 react 官方认为我们(开发者)也许在这三个函数中编写了有副作用的代码,所以要替换掉这三个生命周期,因为这三个生命周期可能在一次 render 中被反复调用多次
六、捕获错误(一般了解)
1、static getDerivedStateFromError
语法
import React from "react";
export default class GetDerivedStateFromError extends React.Component {
/**
*
* @param {*} error 错误信息
* @returns
* 它接收抛出的错误作为参数并且需要返回值用于更新 State
*/
static getDerivedStateFromError(error) {
return
}
render () {
return (
<div>
<div>1</div>
</div>
)
}
}
触发时机
该生命周期函数会在子孙组件抛出错误时执行
使用场景+注意点
该生命周期函数中可用于修改 state 以显示错误提醒的 UI,或者将错误信息发送到服务端进行 Log 用于后期分析;
在捕获到错误的瞬间,React 会在这次渲染周期中将这个组件渲染为 null,这就有可能导致他的父组件设置他上面的 ref 获得 null 而导致一些问题
和componentDidCatch相比
这个生命周期函数与 getDerivedStateFromProps 类似,唯一的区别是他只有在出现错误的时候才触发,他相对于 componentDidCatch 的优势是在当前的渲染周期中就可以修改 State,以在当前渲染就可以出现错误的 UI,而不需要一个 null 的中间态。
而这个方法的出现,也意味着以后出现错误的时候,修改 state 应该放在这里去做,而后续收集错误信息之类的放到 componentDidCatch 里面
例子
import React from "react";
import AComponent from './a'
export default class ErrorComponent extends React.Component {
state = {
isShowError: false
}
/**
*
* @param {*} error 错误信息
* @returns
* 它接收抛出的错误作为参数并且需要返回值用于更新 State
*/
static getDerivedStateFromError(error) {
console.log(error)
return {
isShowError: true
}
}
render () {
if (this.state.isShowError) {
return (
<div>组件内部发生错误</div>
)
}
return (
<div>
<div>1</div>
{/* 当前组件会触发一个错误 */}
<AComponent></AComponent>
</div>
)
}
}
2、componentDidCatch
语法
componentDidCatch () {
}
触发时机
该生命周期函数会在子孙组件抛出错误时触发。
相关使用和 getDerivedStateFromError 几乎一致
3、错误边界(Error Boundary)
是 React 组件,并不是损坏的组件树。错误边界捕捉发生在子组件树中任意地方的 JavaScript 错误,打印错误日志,并且显示回退的用户界面。错误边界捕捉渲染期间、在生命周期方法中和在它们之下整棵树的构造函数中的错误。
可以定制一个只有 componentDidCatch 生命周期函数的 ErrorBoundary 组件。当捕获错误,则显示错误提示,如果没有捕获到错误,则显示子组件。
将需要捕获错误的组件作为 ErrorBoundary 的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被 ErrorBoundary 捕获
import React, { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
render() {
if (this.state.hasError) {
return <h1>发生错误.</h1>;
}
return this.props.children;
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
}
}
export default ErrorBoundary;
定制一个只有 componentDidCatch 生命周期函数的 ErrorBoundary 组件。当捕获错误,则显示错误提示,如果没有捕获到错误,则显示子组件。
将需要捕获错误的组件作为 ErrorBoundary 的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被 ErrorBoundary 捕获
```jsx
import React, { Component } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
render() {
if (this.state.hasError) {
return <h1>发生错误.</h1>;
}
return this.props.children;
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
}
}
export default ErrorBoundary;