如果你是一名前端开发者,并且使用React开发框架,那么你一定绕不开React的一个核心概念——组件生命周期。 组件的生命周期描述的是一个组件从创建,渲染,加载,显示到卸载的整个过程。其大致过程如下:
图片来源请戳此链接:http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
我们从上图可以得知,一个组件主要包括三种行为,分别是:创建,更新,卸载。每个行为又可以划分为:Render
渲染阶段和 Commit
阶段 (更新时存在Pre-Commit
阶段)。这里先简单了解一下生命周期的概况,后文会进行详细描述。
React
官方开发者在生命周期的每个阶段都提供了相关的API,我们可以使用这些API定义组件在某个生命周期进行执行相关的行为。比较常用的就是使用componentDidMount
API,在该函数内部获取来自服务器的数据。
学习这些API函数的使用并了解组件的生命周期的运行机制,是一个React开发者的基本功!
但是,我们必须不能只注重 API 的使用,而忽略了对生命周期的过程理解,API 只是 API,随时都有可能被替代!
这不,在 React v16.8 版本中,横空出世的 Hooks
(俗称钩子)颠覆了之前的类组件范式,让函数组件逐渐成为主流,越来越多的团队开始考虑基于Hooks
进行项目的重构,过去那种在类组件中调用各种生命周期API函数的方法将会慢慢成为过去(不过现在还是得学呀…)
本文不打算进行详述Hooks
的用法及优越性,已经有许多优秀的前端团队对其展开了极富深度的论述啦,我只是一个刚入门不久的前端菜鸟,暂时难以从高层的视角来评判,如果你感兴趣,本文在第四部分推荐阅读给出一些高质量的链接,你可以学到更多。
回到主题。Hooks
提供了一套新的阐释组件生命周期的方式 ,原先许多生命周期的 API 都有了对应的 Hooks
解决方案——使用强大的useEffect
这一Hooks
函数。
综上,本文主要关注以下内容:
useEffect
来替代类组件的生命周期API。攥写本文的初衷是,本人最近在经常使用React
框架开发,对生命周期这一核心概念有必要进行一番梳理,同时也发现网上关于讲解如何使用新特性Hooks
来定制生命周期行为的博文数目较少而且不全面,所以我花费几个小时的时间来整理这些概念,总结方法并提供具体的代码实例,希望能帮助到大家,互相学习。
这里先放我从谷歌搜到的一张大家都在用的生命周期图(图源见链接),直接从API入手描述整个组件的生命周期过程,如果你已经使用过相关的API,那相当明了,但考虑到初学者,我还是基于个人理解给大家归纳总结一下,帮助大家理解、记忆和使用。
本文将API归为三类:
Mount
:挂载APIUpdate
: 更新APIUnmount
:卸载API结合上述流程图,我们从这三类API开始展开叙述,简单讲解一些不常用的API (不完全覆盖),多把文字放在常用的API(基本覆盖)上。
挂载一个组件的过程是先实例化创建该组件并渲染,然后读取DOM使得组件运行在浏览器中。整个过程涉及到的 API 函数如下:
当组件在客户端实例化首次创建时,以下方法依次被调用:
getDefaultProps
(ES5语法,过时)getInitialState
(ES5语法,过时)componentWillMount
(弃用)render
(必须使用)componentDidMount
(常用)当组件属于服务端渲染时,以下方法依次被调用:
getDefaultProps
getInitialState
componentWillMount
render
服务端渲染没有 componentDidMount
方法时因为其执行阶段是组件挂载(插入DOM
树)之后,才会执行,发生在客户端。
但需要提醒的是,getDefaultProps
和 getInitialState
方法仅适用于非ES6
语法情况,在ES6
语法中我们使用 constructor
函数来代替 getInitialState
函数,而getDefaultProps
则可以通过手动赋值指定props
属性,但已不属于函数方法了。(来源官方文档不适用ES6),但是考虑到可能还有些人依旧会使用ES5
语法,这里也会简单介绍这两个函数。(后续若非特别指明,否则讨论范围都在ES6
语法之内)
同时在图一,我们可以得知在constructor
之后,render
之前会有一个getDerivedStateFromProps
函数,这个函数在挂载和更新阶段都有可能调用,我们放在update
部分来讲。
所以下文我将介绍上述6
个函数,包括函数的作用,方法和一些注意事项。
这是目前常用的一个函数,我们编写的React
类组件都是React.Component
基类的继承,组件进行实例化的时候,都会调用其构造函数;如果你不初始化 state
,不绑定方法的话,你不需要为 React
组件实现构造函数(它会调用默认构造函数)。
而且在构造函数的开头,你需要调用 super(props)
, 不然会有许多蜜汁bug
难以定义和识别;
通常构造函数的作用就是:
state
变量setState
方法,我们仅需要为 this.state
赋初始值即可。一个常见的例子如下:
// credit to https://react.docschina.org/docs/react-component.html#constructor
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = {
counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
这种方法其实只有在使用非ES6
语法定义组件(即采用ES5
语法 React.createClass
定义组件)的时候才会出现,这是一种声明默认组件属性(props)
的方法。该函数只会调用一次。
一个简单的例子:
// credit to https://www.cnblogs.com/MuYunyun/p/6629096.html#_lab2_0_0
var MyTitle = React.createClass({
getDefaultProps : function () {
return {
title : 'Hello World' // 声明默认的属性title
};
},
render: function() {
return <h1> {
this.props.title} </h1>; // 调用props属性
}
});
ReactDOM.render(
<MyTitle />,
document.body
);
而在ES6
语法中,这个函数已经被弃用了,起同样作用的写法如下:
// credit to
// https://zh-hans.reactjs.org/docs/react-without-es6.html#setting-the-initial-state
class Greeting extends React.Component {
// ...
}
Greeting.defaultProps = {
name: 'Mary'
};
在ES5
语法中,这个函数的作用相当于初始化每个组件实例的state
变量;在ES6
语法中,我们可以通过在constructor
函数里头对this.state
赋初始值,以起到同样的效果。
getInitialState
和getDefaultProps
的区别在于,getDefauProps
对于组件类来说只会调用一次,而getInitalState
是在每个组件实例化的时候都会调用,并且只调用一次。而props和state的区别在于props
是通过父组件传递,在所有实例中共享,而state
只存在组件内部,保存组件的某些状态。
这里提供一个代码实例帮助理解:
var LikeButton = React.createClass({
getInitialState: function() {
return {
liked: false};
},
handleClick: function(event) {
this.setState({
liked: !this.state.liked});
},
render: function() {
var text = this.state.liked ? 'like' : 'haven\'t liked';
return (
<p onClick={
this.handleClick}>
You {
text} this. Click to toggle.
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
这个函数在进行组件渲染之前调用,这个函数内部进行setState
时,并不会触发额外的渲染(合并到在下一步的render
中执行)。因为此方法在组件的生命周期中只调用一次,而这恰好发生在组件初始化之前。因此,无法访问DOM
。
当你使用服务端渲染的时候,这是服务器端惟一调用的生命周期方法。
不过这个函数在较新版本的React中已经被视为不安全(unsafe)
,官方建议我们不使用该函数。详见(UNSAFE_componentWillMount())。
这个方法是特别不常用并且不建议使用的。仅提供一个简单实例:
componentWillMount() {
let mode;
if (this.props.age > 70) {
mode = 'old';
} else if (this.props.age < 18) {
mode = 'young';
} else {
mode = 'middle';
}
this.setState({
mode });
}
这个方法是最常用,而且在类组件中是必用的方法。该方法用于创建一个虚拟DOM
,用来表示组件的结构。需要注意几点的就是:
props
和state
来访问数据,不能修改;null
,false
或其他react
组件;标签变成一个组件;
- 无法改变组件的状态,
class
组件中只能通过setState
方法改变。
这里不给出实例,见上面代码即可。
2.1.6 componentDidMount
当组件挂载(Mount)
到DOM
树上但并未显示到浏览器上时,这个函数方法将会被立即调用。
一些依赖于DOM
节点初始化的操作应该要放在这里,如常见的向服务器请求数据。
我们通常在这个函数内部获取数据,然后通过setState
的方法触发额外的渲染,也就是说从构造到浏览器运行组件可能会触发两次的render
,但是用户并不会看到中间的状态,因为此时的浏览器并未更新屏幕。
不过这个方法内部的数据请求过于庞大可能会引发性能问题,需要谨慎使用。
虽然我们也可以在componentWillMount
函数中请求数据,但是官方推荐我们使用这个函数进行数据的异步请求。详细看官方解释fetching-external-data。
我摘取其重要部分代码和内容,浓缩如下:
先看两个函数的使用实例。
// Before 使用componentWillMount
class ExampleComponent extends React.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 使用componentDidMout
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
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 ...
}
}
}
官方推荐使用componentDidMount
处理组件异步请求的理由如下:
- 我们知道服务端渲染
(SSR)
的时候,不会执行componentDidMount
但是会执行componentWillMount
,此时如果数据请求卸载componentWillMount
时,服务器和客户端将会执行两次请求,使用componentDidMount
API将会减少不必要的请求。
- 在服务段渲染的时候,使用
componentWillMount
时可能有服务端内存泄漏(出现不调用componentWillmount
)以及渲染中断,导致异步渲染(渲染中断导致componentWillMount
不调用)的情况。
- 从
react16.3
开始 componentWillMount
API被视为不安全,逐渐弃用。
同时官方也推荐在componentDidMount
进行事件订阅的操作。有一点要注意的是如果你在componentDidMount
使用了订阅事件,那么你要在卸载API componentWillUnmount
中取消订阅。请求发送数据同理。
下面是一段实例代码,帮助你理解。
// After
class ExampleComponent extends React.Component {
state = {
subscribedValue: this.props.dataSource.value,
};
componentDidMount() {
// Event listeners are only safe to add after mount,
// So they won't leak if mount is interrupted or errors.
this.props.dataSource.subscribe(
this.handleSubscriptionChange
);
// External values could change between render and mount,
// In some cases it may be important to handle this case.
if (
this.state.subscribedValue !==
this.props.dataSource.value
) {
this.setState({
subscribedValue: this.props.dataSource.value,
});
}
}
componentWillUnmount() {
this.props.dataSource.unsubscribe(
this.handleSubscriptionChange
);
}
handleSubscriptionChange = 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;(常用)
同时我们还会介绍 getDerivedStateFromProps
这一函数,它在挂载和更新都可以使用,但不是常用函数。
2.2.1 componentWillReceiveProps
这个函数会在已挂载的组件接受新的props
之前被调用,如果你需要更新状态以及相应prop
的更改,
那么你需要比较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.2.2 getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
这个函数是新版本的react
中提出来的,会在调用render
方法之前调用,并且在初始挂载和后续更新过程中都会被调用,它应该返回一个对象来更新state
,如果返回null
,那么就不想需要更新内容。
不过这个函数也处于不常用的状态,原因是会带来代码的冗余。官方给出了一些替代的方案。
派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:
- 如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改,请改用componentDidUpdate。
- 如果只想在 prop 更改时重新计算某些数据,请使用 memoization helper 代替。
- 如果你想在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控 代替。
这个方法每次渲染前都会触发,不同于 componentWillReceiveProps
仅在父组件重新渲染时进行触发。图一指出了更新阶段的三种情况下,这个函数会被触发:
setState()
方法
props
改变
forceUpdate
方法调用
给出一个官方实例:
// credit to https://react.docschina.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#when-to-use-derived-state
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// Any time the current user changes,
// Reset any parts of state that are tied to that user.
// In this simple example, that's just the email.
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}
2.2.3 shouldComponentUpdate
这个函数发生在上面的函数 componentWillReceiveProps
之后,其返回值true or false
用于判断当前的state
和props
变化是否需要触发组件的更新。
默认行为是 state
每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。
这个函数是一个不常用的函数,如果你想避免一些无谓的渲染以提升性能的话,那么可以考虑使用它。
用法比较简单,一般是在函数内部添加一些比较条件即可。
给出一个简答的例子帮助理解。
// credit to https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/using_should_component_update.html
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
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;
}
// Test for A's keys different from B.
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, nextState) {
return (
!shallowEqual(instance.props, nextProps) ||
!shallowEqual(instance.state, nextState)
);
}
var ReactComponentWithPureRenderMixin = {
shouldComponentUpdate: function(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
},
};
2.2.4 componentWillUpdate
当组件收到新的props
或state
,经过函数shouldComponentUpdate
确认允许组件更新之后,这个函数会在组件更新渲染之前被调用。
这个函数在更新渲染前被使用,初始挂载阶段的渲染将不会调用此方法。这个方法中不能调用setState
方法,而且也不能执行任何操作触发对 react
组件的更新。
不过这个方法已经被新版本的react
标记为不安全,属于弃用状态。
不过还是提供一个简单的例子,如下:
// credit to https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/tapping_into_componentwillupdate.html
// dispatching an action based on state change
componentWillUpdate(nextProps, nextState) {
if (nextState.open == true && this.state.open == false) {
this.props.onWillOpen();
}
}
2.2.6 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>
);
}
}
2.2.5 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。
2.3.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
useEffect作用于渲染后!!所以只能替代这三个render阶段后的 lifecycle API~
详细的useEffect
使用请看官方文档: 使用useEffect hook,文档中给出了比较详细的useEffect
的API解释,这里我就不赘述了。理解完上面的概念之后,看下面几个经过简化后的例子之后,我们就可以快速进行迁移使用useEffect
来替代原来的API啦。
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')); // 不需要指定第二个参数
值得一提的是,现在官方推荐的编程规范就是不区分 update
阶段和 mount
阶段,两个阶段视为一致。
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的时候都会执行,我们可以使用一些条件参数来跳过执行。就上面最后一个例子,我们可以传入第二个参数,判断前后参数是否一致,若一致则执行,否则就跳过。
至于其他比较不常用的生命周期函数,现在useEffect
可能还做不到那种细粒度的操作(尤其是限制发生在render
之后),但是相信官方一定会不断完善这方面的支持滴!敬请期待!
四、 React Hooks 推荐阅读
- 精读 React 16 新特性;
- 精读 React Hooks;
- Making sense of react hooks
- 对 React Hooks 的一些思考
参考自以下链接,并鸣谢:
- 深入React组件生命周期
- React 中文文档 Hooks useEffect 部分
- The Lifecycle of a React Component
- React.Component 官方文档
- react组件何时请求异步数据——componentWillMount或者componentDidMount?
- react:不常用的声明周期方法
- react: 使用useEffect
如果本文有哪些地方出现了纰漏错误,还请大家大方指出,帮助本文完善得更好,谢谢!
感谢你的阅读!