一、简介
作为一名使用react框架的前端开发工作者,那么react的一个核心概念–组件生命周期就显示十分重要。组件的生命周期描述的是一个组件从创建、渲染、加载、显示到卸载的整个过程,其大致的过程如下:
图片来源请戳此链接:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
从图中,我们也可以看到,一个组件主要包括了三种行为:创建、更新、卸载。每一种行为又可以划分为:Render渲染阶段和Commit阶段。
React官方开发者在生命周期的每一个阶段都提供了API,我们可以使用这个API定义组件在某个生命周期执行相关的行为。
不过技术是会被不断往方便快捷的方向改进的。
在React v16.8版本中,引进了**Hooks(俗称钩子)**颠覆了之前的类组件范式,让函数组件逐渐成为了主流,越来越多的团队开始考虑基于Hooks进行项目的重构,过去那种在类组件中调用各种生命周期API函数的方法将会成为过去。
Hooks提供了一套新的阐述组件生命周期的方式,原先许多生命周期的API都有了对应的Hooks解决方案----使用强大的useEffect这一 Hooks函数。
二、组件生命周期过程及API详解
首先我先放一张图,详细列述了 React 组件在整个生命周期中所涉及的方法和行为:
本文将API归为三类:
1、Mount: 挂载API
(1) constructor()
(2) static getDerivedStateFormProps()
(3) render()
(4) componentDidMount()
注意:
下述生命周期方法即将过时,在新代码中应该避免使用它们:
UNSAFE_componentWillMount()
2、Update:更新API
当组件的props或state发生变化时会触发更新,组件更新的生命周期调用顺序如下:
(1) static getDerivedStateFormProps()
(2) shouldComponentUpdate()
(3) render()
(4) getSnapshotBeforeUpdate
(3) comonentDidMount()
注意:
下述方法即将过时,在新代码中应该避免使用它们:
UNSAFE_componentWillUpdate()
UNSAFE_componentWillReceiverProps()
3、UNmount:卸载API
当组件从DOM中移除时会调用如下方法:comonentDidMount()
2.1 Mount
挂载一个组件的过程是先实例化创建该组件并渲染,然后读取DOM使得组件运行在浏览器中。整个过程涉及到的API函数如下:
当组件在客户端被实例化,第一次被创建时,以下方法依次被调用:
- constructor
- componentWillMount(弃用,在React官方上有解释原因)
- render(必用)
- componentDidMount(常用)
当组件在服务端被实例化,首次被创建时,以下方法依次被调用:
- constructor
- componentWillMount
- render
componentDidMount不会在服务器端被渲染的过程中调用。 因为其执行阶段是组件挂载(插入DOM树)之后,才会执行,发生在客户端。
接下来是对上述函数进行介绍,包括函数的作用、方法和一些注意事项。
(1) constructor()
这是class类中很常见的,我们编写的React类组件都是React.Component基类的继承,组件进行实例化的时候,都会调用其构造函数;如果你不初始化state,不绑定方法的话,你不需要为React组件实现构造函数(它会调用默认构造函数)。
而且在构造函数开头,你需要调用super(props);
通常构造函数的作用就是:
1、初始化内部state变量
2、为事件处理含水率绑定事件实例,
需要注意的是,在构造函数里不要出现setState方法,我们仅需要为this.state赋初始值即可。
一个常见的例子:
constructor(props) {
super(props);
// 不要再这里调用this.setState();
this.state = {counter: 0};
this.handleClick = this.handleClick.bind(this);
}
(2) render()
这个方法很常用,而且在类组件中是必用的方法。改方法用于创建一个虚拟DOM,用来表示组件的结构。需要注意几点就是:
1、只能通过props和 state来访问数据,不能修改
2、支持返回null,false或其他react组件
3、只能返回一个顶级组件,如果出现多个组件,你需要用div标签变成一个组件
4、无法改变组件的状态,class组件中只能通过setState方法改变
(3) componentWillMount()
这个函数在进行组件渲染之前调用,这个函数内部进行setState时,并不会触发额外的渲染(合并到在下一步的render中执行)。因为此方法在组件的生命周期中只调用一次,而这恰好发生在组件初始化之前。因此,无法访问DOM。
当你使用服务端渲染的时候,这是服务器端惟一调用的生命周期方法。
不过这个函数在较新版本的React中已经被视为不安全(unsafe),官方建议我们不使用该函数。详见(https://react.docschina.org/docs/react-component.html#unsafe_componentwillmount)。
因此,这个方法是特别不常用并且不建议使用的。
(4)componentDidMount()
当组件挂载(Monut)到DOM树上但未显示到浏览器上时,这个函数方法将会被立即调用。一些依赖于DOM节点初始化的操作应该被放在这里,如常见的向服务器请求数据。
我们通在这个函数内部获取数据,然后通过setState的方法触发额外的渲染,也就是说从构造到浏览器运行组件可能会触发两次render,但是用户并不会看到中间的状态,因为此时的浏览器并未更新屏幕。
不过这个方法内部的数据请求过于庞大可能引起性能问题,需要谨慎使用。
虽然我们也可以在componentWillMount函数中请求数据,但是官方推荐我们使用这个函数进行数据的异步请求。
先看两个函数使用实例:
// Before use ComponentDidMount
class ExampleComponent extends ReactComponent.Component {
state = {
externalData: null,
};
componentWillMount() {
this._asyncRequest = loadMyAsyncData().then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if(this.state.externalData === null){
// Render loading state...
} else {
// Render real UI ...
}
}
}
// After use ComponentDidMount
class ExampleComponent extends ReactComponent.Component {
state = {
externalData: null,
};
componentDidMount() {
this._asyncRequest = loadMyAsyncData().them(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
conponentDidUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if(this.state.externalData === null ) {
// Render loading state ...
} else {
// Render real UI ...
}
}
}
官方推荐使用componentDidMount处理组件异步请求的理由如下:
1、我们知道服务器端渲染(SSR)的时候,不会执行componentDidMount但是会执行componentWillMount ,此时如果数据请求卸载componentWillMount时,服务器和客户端将会执行两次,使用componentDidMount API将会减少不必要的请求。
2、在服务端渲染的时候,使用componentWillMount时可能有服务端内存泄露(出现不调用)的情况。
3、从React16.3开始 componentWillMount API被视为不安全,逐渐弃用。
同时官方也推荐在componentDidMount进行事件订阅的操作。有一点注意的是如果你在componentDidMount使用了订阅事件,那么你要在卸载API componentWillUnmount中取消订阅。请求发送数据同理。
下面试一段实例代码:
// After
class ExampleComponent extends React.Component {
state = {
subscribeValue: this.props.dataSource.value,
};
componentDidMount() {
this.props.dataSource.subscribe(
this.handleSubscriptiononChange
);
if (
this.state.subscribeValue !== this.props.dataSource.value
) {
this.setState({
subscribedValue: this.props.dataSource.value,
});
}
}
componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptiononChange
);
}
handleSubscriptiononChange = dataSource => {
this.setState({
subscribedValue: dataSource .value,
});
};
}
当componentDidMount函数运行完毕之后,我们的组件就会显示在屏幕上啦。
2.2 Update
我们的组件运行在浏览器的时候,会随着数据状态的变动进行更新。变动主要包括组件自身state的变动,以及父组件传递下来的props的变动。对应的流程图部分如下:
可以看到执行过程类似Mount,都有Will、render、did过程对应的API,但是区别在于挂载中的组件需要根据props和state的变化进行判定是否需要更新(当然通常情况下需要更新)。
上述流程主要包括了以下方法:
- componentWillReceiveProps – props 触发更新API;(弃用)
- shouldComponentUpdate – 确定是否触发更新;(不常用)
- componentWillUpdate – 渲染前的组件更新API;(弃用)
- render – 渲染函数;(必用)
- componentDidUpdate – 渲染后更新的API;(常用)
(1) componentWillReceiveProps
这个函数会在已挂载的组件接受新的props之前被调用,如果你需要更新状态以及相应props的更改,那么你需要比较this.props和nextProps并在这个函数中使用this.setState()执行state转换。但在Mount阶段不会使用。
需要明白的是,只要父组件重新渲染,那么即使props没有更改,本方法也会调用。
虽然本方法是处于弃用(官方标记为UNSAFE)的状态,但是也有一个重要的好处,就是可以定义子组件接受父组件props之后的状态和行为。
下面一个简单例子:
//这种方式十分适合父子组件的互动,通常是父组件需要通过某些状态控制子组件渲染亦或销毁...
// credit to https://juejin.im/post/5a39de3d6fb9a045154405ec
componentWillReceiveProps(nextProps) {
//componentWillReceiveProps方法中第一个参数代表即将传入的新的Props
if (this.props.sharecard_show !== nextProps.sharecard_show){
//在这里我们仍可以通过this.props来获取旧的外部状态
//通过新旧状态的对比,来决定是否进行其他方法
if (nextProps.sharecard_show){
this.handleGetCard();
}
}
}
父组件通过setState的方法触发更新渲染(可能不会改变子组件的props),从而触发上述的函数。
(2)getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
这个函数是新版本的react中提出来的,会在调用render方法之前调用,并且在初始挂载和后续更新过程中都会被调用,它应该返回一个对象来更新state,如果返回null,那么就不想需要更新内容。
不过这个函数也处于不常用的状态,原因是会带来代码的冗余。官方给出了一些替代的方案。
这个方法每次渲染前都会触发,不同于 componentWillReceiveProps仅在父组件重新渲染时进行触发。图一指出了更新阶段的三种情况下,这个函数会被触发:
- setState()方法
- props 改变
- forceUpdate方法调用
给出一个官方实例:
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFormProps(props, state) {
if (props.userID !== state.prevPropsUserId) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
}
(3) shouldComponentUpdate
这个函数发生在上面的函数componentWillReceiverProps之后,其返回值true or false用于判断当前的state和props变化是否需要触发组件的更新。
默认行为是state每次发生变化组件都会重新渲染。大部分情况下,你应该都会遵循默认行为。
这个函数是一个不常用的函数,如果你想要避免一些无谓的渲染以提升性能的话,那么可以考虑使用它。
用法比较简单,一般是在函数内部添加一些比较条件即可。
给出一个简单的例子理解:
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (objA === objB) {
return true;
}
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
var bHasOwnProperty = hasOwnProperty.bind(objB);
for (var i = 0; i < keysA.length; i++) {
if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
return false;
}
}
return true;
}
function shallowCompare(instance, nextProps, nextProps) {
return(
!shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState)
);
}
var ReactComponentWillPureRenderMixin = {
shouldComponentUpdate: function(nextProps, nextProps) {
return shallowCompare(this, nextProps, nextState);
}
}
(4)componentWillUpdate
当组件收到新的props或state,经过函数shouldComponentUpdate确认允许组件更新之后,这个函数会在组件更新渲染之前被调用。
这个函数在更新渲染前被使用,初始挂载阶段的渲染将不会调用此方法。这个方法中不能调用setState方法,而且也不能执行任何操作触发对 react组件的更新。
不过这个方法已经被新版本的react标记为不安全,属于弃用状态。
不过还是提供一个简单的例子,如下:
componentWillUpdate(nextProps, nextState) {
if (nextState.open == true && this.state.open == false) {
this.props.onWillOpen();
}
}
(5) getSnapshotBeforeUpdate
这个函数比较不常用,在最近一次渲染输出(提交到 DOM 节点)之前被调用(render之后)。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。
从图一中可看出,这个函数发生在render之后,属于一个特殊的pre-commit阶段,可以读取DOM数据。
贴一个官网的简单实例,关于如何捕获滚动位置并利用:
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
(6) componentDidUpdate
这个函数是最常用的了,会在更新并且shouldComponentUpdate返回true的情况下,render之后调用,但是mount阶段的render则不会执行此方法。
组件进行更新之后,我们可以在这个函数中对DOM进行操作,以及setState()操作(需要注意包裹在条件语句中,不然一直处于setState更新状态导致死循环),同时可以根据前后的props差别来进行网络请求,这一点类似于componentDidMount。
再提醒一遍,函数内部需要有条件约束才能进行DOM操作,setState和获取数据,不然会导致一直更新死循环!
给出官方一个实例:
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
2.3 unmount
这个阶段比较好理解,就是组件从DOM树上销毁卸载的过程,只涉及一个 componentWillUnmount API。
(1) componentWillUnmount
我们通常会在此方法中执行必要的清理操作,如取消网络请求,移除事件订阅等,而且要不应该调用setState()方法。
这个阶段我们就只负责清理就好了!一般和componentDidMount和componentDidUpdate搭配一起出现。
摘取上面一个例子。
componentDidMount() {
this.props.dataSource.subscribe(
this.handleSubscriptionChange
);
if (
this.state.subscribedValue !==
this.props.dataSource.value
) {
this.setState({
subscribedValue: this.props.dataSource.value,
});
}
}
componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptionChange
);
}
到此,传统但重要的生命周期API已经基本介绍完毕啦。
三、使用useEffect 方法替代生命周期API
useEffect是React新版本推出的一个特别常用的hooks功能之一,useEffect可以在组件渲染后实现各种不同的副作用,他使得函数式组件具备编写类组件生命周期的功能。在这里仅仅介绍三个常用的生命周期替代方案,分别是:
- componentDidMount VS useEffect
- componentDidUpdate VS useEffect
- componentWillUnmount VS useEffect
3.1 componentDidMount vs useEffect类
组件中,我们这样编写componentDidMount:
class Example extends React.Component {
componentDidMount() {
console.log('Did mount!');
}
render() {
return null;
}
}
在函数组件中,我们可以使用useEffect这样编写:
function Example() {
// 注意不要省略第二个参数 [],这个参数保证函数只在挂载的时候进行,而不会在更新的时候执行。
useEffect(() => console.log('mounted'), []);
return null;
}
3.2 componentDidUpdate vs useEffect
类组件中,我们这样编写componentDidUpdate:
componentDidMount() {
console.log('mounted or updated');
}
componentDidUpdate() {
console.log('mounted or updated');
}
而在函数组件中,我们使用useEffect起到同样的效果:
useEffect(() => console.log('mounted or updated')); // 不需要指定第二个参数
3.3 componentWillUnmount vs useEffect
类组件中,我们这样编写componentWillUnmount:
componentWillUnmount() {
console.log('will unmount');
}
而在函数组件中,我们使用useEffect起到同样的效果:
useEffect(() => {
return () => {
console.log('will unmount'); // 直接使用return返回一个函数,这个函数在unmount时执行。
}
}, []);
你也可以使用useEffect 组合componentDidMount 和 componentDidUnmount。
useEffect(()=>{
console.log("mounted");
return () => {
console.log("unmounted");
}
}, [Started]) // 前后两次执行的Started相等时,useEffect代码生效,否则跳过。
这里普及useEffect的两点小tricks:
1.就功能而言,使用多个useEffect实现代码关注点分离。我们在一个函数组件内部可以不用将所有功能不一致的代码都塞在一个 componentDidMount里头,我们就功能而言多次在一个组件内部使用useEffect,这样会使得代码更加的简洁耐看。
2.使用条件跳过不必要的useEffect执行,实现性能优化。由于useEffect在每次mount或者update的时候都会执行,我们可以使用一些条件参数来跳过执行。就上面最后一个例子,我们可以传入第二个参数,判断前后参数是否一致,若一致则执行,否则就跳过。
四、参考资料:
https://blog.csdn.net/CVSvsvsvsvs/article/details/91410447/
https://zh-hans.reactjs.org/docs/react-component.html#componentdidmount
https://www.jianshu.com/p/b98f2d365b28