前言
本篇文章主要内容针对
React
类组件的生命周期展开,会详细介绍生命周期 “钩子” 的执行和用法,如果一点也不了解React
的同学建议先学习一下React
比较基础的内容 React 基础篇 —— 带你走进 React 世界。
创建项目
首先使用 create-react-app
脚手架创建一个 React
项目,脚手架工具的安装和项目创建命令如下:
# 安装脚手架
$ npm install -g create-react-app
# 创建项目
$ create-react-app life-cycle
创建项目后删除 src
目录中的无用文件,只留下 index.js
入口文件即可。
类组件的生命周期
静态属性 defaultProps
defaultProps
是用来给 React
类组件设置参数初始值的,也是最早执行的,算不算生命周期说法不一,但是觉得有必要说一下,因为在 React 15.x
版本的时候可以用 React.createClass
创建类组件,组件中有与 defaultProps
静态属性作用相同的生命周期 “钩子” getDefaultProps
,随着 React 16.x
版本废弃了 React.createClass
,也就使用 defaultProps
属性替代了 getDefaultProps
。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
static defaultProps = {
num: 0
}
render() {
return <div>{this.props.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
启动项目后,发现页面上成功的渲染了节点中的数字,这说明设置初始值生效了。
constructor 方法
constructor
是 ES6
中类的写法中给实例设置属性的钩子,在类的实例被创建时执行,下面是对比 defaultProps
静态属性执行顺序的代码。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
console.log(props.number); // 0
}
static defaultProps = {
num: 0
}
render() {
return <div>{this.props.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
从上面案例中可以看到当执行 constructor
时,props
对象中的 num
属性已经有值了,这也充分说明了说明 constructor
是晚于 defaultProps
执行的。
状态对象 state
在 React
中,每一个类组件都有一个属于自己的状态,可以使用 setState
方法更新状态,在 React 15.x
中,通过 React.createClass
创建类组件,使用对应的生命周期 “钩子” getInitialState
来创建,同样的,React 16.x
废弃了 React.createClass
,创建 state
的过程自然由新的方式代替。
创建 state
的方式大概有两种,分别是在 constructor
中创建或者直接创建 state
属性,代码如下:
/* 第一种创建 state 的方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
this.state = { num: 0 };
}
render() {
return <div>{this.state.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
/* 第二种创建 state 的方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
console.log(this.state.num); // 0
}
// 创建 state
state = { num: 0 }
render() {
return <div>{this.state.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
从上面可以看出直接创建 state
属性的方式与创建静态属性 defaultProps
类似,执行要早于 constructor
。
componentWillMount 钩子
componentWillMount
生命周期 “钩子” 在组件将要挂载时执行,也就是说在组件挂载前会调用 componentWillMount
,整个组件的生命周期中只执行一次,一般用于发送当前组件需要的 Ajax
请求获取数据。
在 React 16.3
版本中标识了该 “钩子” 会被在未来版本中废弃,目前仍然可以使用,在 componentWillMount
的可以迁移到 constructor
,但不能包含 setState
操作,因为 constructor
中无法调用 setState
。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
console.log('constructor');
}
state = { num: 0 }
componentWillMount() {
console.log('componentWillMount');
this.setState({ num: 3 });
}
render() {
return <div>{this.state.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
// constructor
// componentWillMount
从上面的打印结果可以看出 componentWillMount
“钩子” 的执行是晚于 constructor
的,从页面渲染 3
的结果来看,在 componentWillMount
“钩子” 中已经可以使用 setState
更改状态了。
render 钩子
render
钩子的主要作用是返回组件内部要被渲染的 JSX
,即所谓的挂载过程,将上面例子简单修改一下。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
console.log('constructor');
}
state = { num: 0 }
componentWillMount() {
console.log('componentWillMount');
}
render() {
console.log('render');
return <div>{this.state.num}</div>;
}
}
ReactDOM.render(<Counter />, window.root);
// constructor
// componentWillMount
// render
从打印结果可以看出 constructor
最先执行,其次是 componentWillMount
,最后是 render
,由于状态或属性的更新可能导致组件重新渲染,所以 render
可能会被执行多次。
componentDidMount 钩子
componentDidMount
生命周期 “钩子” 在组件挂载后执行,一般会将一些依赖于 DOM
的操作放在该 “钩子” 内执行,整个生命周期只执行一次。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
constructor(props) {
super();
console.log('constructor');
}
state = { num: 0 }
componentWillMount() {
console.log('componentWillMount');
}
componentDidMount() {
console.log('componentDidMount');
}
render() {
console.log('render');
return <div>{this.state.num}</div>
}
}
ReactDOM.render(<Counter />, window.root);
// constructor
// componentWillMount
// render
// componentDidMount
执行顺序:constructor
→ componentWillMount
→ render
→ componentDidMount
。
componentWillUpdate 钩子
在调用 setState
更新数据后会触发 render
钩子对组件重新渲染,在执行 render
前会调用 componentWillUpdate
钩子,即将要更新时执行(此时状态和页面都没更新),钩子默认有三个参数,分别为 nextProps
、nextState
和 nextContext
,即更新后的属性对象、状态对象和上下文对象。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('componentWillUpdate');
console.log('nowState', this.state);
console.log('nextProps', nextProps);
console.log('nextState', nextState);
console.log('nextContext', nextContext);
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// componentWillUpdate
// nowState { num: 0 }
// nextProps {}
// nextState { num: 1 }
// nextContext {}
// render
从执行点击事件后的结果来看,在重新渲染之前 componentWillUpdate
早于 render
执行,而在 componentWillUpdate
执行时 state
的状态还未更新。
componentDidUpdate 钩子
在调用 setState
更新数据后执行 render
钩子对组件重新渲染,渲染后会立即调用 componentDidUpdate
钩子,此时 state
状态和页面都已经更新,钩子默认有三个参数,分别为 prevProps
、prevState
和 prevContext
,即更新前的属性对象、状态对象和上下文对象。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('componentDidUpdate');
console.log('nowState', this.state);
console.log('prevProps', prevProps);
console.log('prevState', prevState);
console.log('prevContext', prevContext);
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// componentWillUpdate
// render
// componentDidUpdate
// nowState { num: 1 }
// prevProps {}
// prevState { num: 0 }
// prevContext {}
触发点击事件后的执行顺序为:componentWillUpdate
→ render
→ componentDidUpdate
。
shouldComponentUpdate 钩子
在使用 setState
更改状态时,其实还会默默的执行 shouldComponentUpdate
“钩子”,该钩子有返回值,不使用该 “钩子” 的情况下默认返回值为 true
,若使用该 “钩子” 必须指定布尔类型的返回值 true
或 false
,当返回值为 true
时代表更新状态和视图,否则不更新,只要使用 setState
就会触发该 “钩子”,该钩子有三个参数,与 componentWillUpdate
“钩子” 相同。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('componentDidUpdate');
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
console.log('shouldComponentUpdate');
console.log('nowState', this.state);
console.log('nextProps', nextProps);
console.log('nextState', nextState);
console.log('nextContext', nextContext);
return true;
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// shouldComponentUpdate
// nowState { num: 0 }
// nextProps {}
// nextState { num: 1 }
// nextContext {}
// componentWillUpdate
// render
// componentDidUpdate
当 shouldComponentUpdate
“钩子” 返回值为 true
时,触发点击事件后的执行顺序为:shouldComponentUpdate
→ componentWillUpdate
→ render
→ componentDidUpdate
。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('componentDidUpdate');
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
console.log('shouldComponentUpdate');
console.log('nextState', nextState);
return false;
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// shouldComponentUpdate
// nextState { num: 1 } 不断更新
当 shouldComponentUpdate
“钩子” 返回值为 false
时,触发点击事件后只有 shouldComponentUpdate
执行了,并且随着触发点击事件的次数增加,nextState
参数的状态不断变化,但是 state
和页面都不更新。
componentWillUnmount 钩子
componentWillUnmount
“钩子” 会在组件卸载之前触发,卸载组件需调用 ReactDOM
的 unmountComponentAtNode
方法,并传入一个根节点,将会卸载这个根节点内部的所有组件。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
// 卸载组件
ReactDOM.unmountComponentAtNode(window.root);
}
componentWillUnmount () {
console.log('componentWillUnmount');
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>Kill</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// componentWillUnmount
componentWillUnmount
钩子一般用来在卸载组件之前清除可能会调用 setState
的异步操作,为了防止在卸载组件后继续更新状态而报错。
复合组件的生命周期
上面着重介绍了单个类组件的生命周期,有的生命周期由于一个组件不容易演示,所以放在了这节中,这节也会将复合组件的生命周期执行顺序进行分析,并阐明一些使用的注意事项。
复合组件渲染生命周期的执行顺序
在复合组件中,父组件套着子组件,两个组件都有自己的生命周期,那么执行顺序会是怎么样的,看下面案例。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 子组件
class ChildCounter extends Component {
state = { num: 0 }
componentWillMount() {
console.log('child-componentWillMount');
}
componentDidMount() {
console.log('child-componentDidMount');
}
render() {
console.log('child-render');
return <div>{this.state.num}</div>
}
}
// 父组件
class Counter extends Component {
componentWillMount() {
console.log('parent-componentWillMount');
}
componentDidMount() {
console.log('parent-componentDidMount');
}
render() {
console.log('parent-render');
return (
<div>
<ChildCounter />
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// parent-componentWillMount
// parent-render
// child-componentWillMount
// child-render
// child-componentDidMount
// parent-componentDidMount
从上面的执行顺序可以看出,在执行父组件生命周期的时候,执行 render
会渲染子组件,渲染子组件会将子组件的生命周期优先执行,等子组件完成渲染继续父组件的渲染,即继续执行父组件渲染后的生命周期。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 子组件
class ChildCounter extends Component {
state = { num: 0 }
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('child-componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('child-componentDidUpdate');
}
handleClick = () => {
this.setState({ num: this.state.num - 1 });
}
render() {
console.log('child-render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>update-child</button>
</div>
)
}
}
// 父组件
class Counter extends Component {
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('parent-componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('parent-componentDidUpdate');
}
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
render() {
console.log('parent-render');
return (
<div>
<ChildCounter />
<button onClick={this.handleClick}>update-parent</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// 点击子组件更新按钮
// child-componentWillUpdate
// clild-render
// child-componentDidUpdate
// 点击父组件更新按钮
// parent-componentWillUpdate
// parent-render
// child-componentWillUpdate
// clild-render
// child-componentDidUpdate
// parent-componentDidUpdate
当子组件更新时,父组件不会重新渲染,只会执行子组件的生命周期,当父组件更新时,子组件也会重新渲染,此时当父组件执行 render
时会执行子组件更新相关的生命周期,在继续执行父组件更新相关的生命周期。
点击父组件更新按钮生命周期的执行顺序:parent-componentWillUpdate
→ parent-render
→ child-componentWillUpdate
→ clild-render
→ child-componentDidUpdate
→ parent-componentDidUpdate
。
点击子组件更新按钮生命周期的执行顺序:child-componentWillUpdate
→ clild-render
→ child-componentDidUpdate
。
如果更新父组件时,不希望子组件重新渲染,可以通过子组件的
shouldComponentUpdate
“钩子” 将返回值设置为false
的方式来控制。
componentWillReceiveProps 钩子
当传入组件的参数,即 props
发生变化时,componentWillReceiveProps
“钩子” 执行,该钩子有一个参数,代表下一次更新的 props
对象,执行该 “钩子” 时,props
并没有更新,也就是说是在 props
变化之前执行。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 子组件
class ChildCounter extends Component {
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('child-componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('child-componentDidUpdate');
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
console.log('child-shouldComponentUpdate');
return true;
}
componentWillReceiveProps(nextProps) {
console.log('child-componentWillReceiveProps');
console.log('nowProps', this.props);
console.log('nextProps', nextProps);
}
render() {
console.log('child-render');
return <div>{this.props.n}</div>
}
}
// 父组件
class Counter extends Component {
state = { num: 0 }
componentWillUpdate(nextProps, nextState, nextContext) {
console.log('parent-componentWillUpdate');
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('parent-componentDidUpdate');
}
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
render() {
console.log('parent-render');
return (
<div>
<ChildCounter n={this.state.num} />
<button onClick={this.handleClick}>update-parent</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// parent-componentWillUpdate
// parent-render
// child-componentWillReceiveProps
// nowProps { n: 0 }
// nextProps { n: 1 }
// child-shouldComponentUpdate
// child-componentWillUpdate
// child-render
// child-componentDidUpdate
// parent-componentDidUpdate
点击父组件更新按钮后,父子组件生命周期的执行顺序如下:
parent-componentWillUpdate
→ parent-render
→ child-componentWillReceiveProps
→ child-shouldComponentUpdate
→ child-componentWillUpdate
→ child-render
→ child-componentDidUpdate
→ parent-componentDidUpdate
。
由此可以说明 componentWillReceiveProps
钩子在 shouldComponentUpdate
之前执行。
componentWillReceiveProps
“钩子” 在第一次渲染父子组件时不执行,在React 16.x
版本中被标记为 “已废弃”。
关于 setState 在生命周期中的使用
在 React
生命周期 “钩子” 中,只有 componentWillMount
、componentDidMount
和 componentWillReceiveProps
中可以调用 setState
。
原因是 setState
方法会触发 render
“钩子” 执行,而 shouldComponentUpdate
、componentWillUpdate
、componentDidUpdate
是在 render
后触发,包括在 render
中调用 setState
,都会出现更新 “死循环” 的现象,最后造成堆栈溢出,而 componentWillUnmount
“钩子” 执行时,组件将被卸载,在此时更新状态毫无意义。
在
componentWillReceiveProps
中使用setState
,其目的是为了将新更改的属性更新为该组件的状态,但React
官方不建议这样使用。
React 生命周期流程图
下面是一张关于目前版本比较常用的 React
生命周期 “钩子” 执行顺序的流程图,帮助大家快速理解 React
生命周期中各个钩子函数的执行过程。
React 16.3 新增生命周期
getDerivedStateFromProps 静态方法
getDerivedStateFromProps
是一个类组件的静态方法,用来替代 componentWillReceiveProps
“钩子”,在传入的属性变化之前执行,方法的参数与 componentWillReceiveProps
相同,是更新的属性对象,该方法要求必须返回一个状态对象的返回值,且使用该方法的组件必须含有 state
,不能和 componentWillMount
“钩子” 同时使用。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 子组件
class ChildCounter extends Component {
state = { num: 0 }
componentDidUpdate(prevProps, prevState, prevContext) {
console.log('child-componentDidUpdate');
console.log('nowState', this.state);
}
static getDerivedStateFromProps(nextProps) {
console.log('child-getDerivedStateFromProps');
console.log('nextProps', nextProps);
return { num: nextProps.n };
}
render() {
console.log('child-render');
return <div>{this.props.n}</div>
}
}
// 父组件
class Counter extends Component {
state = { num: 0 }
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
render() {
console.log('parent-render');
return (
<div>
<ChildCounter n={this.state.num} />
<button onClick={this.handleClick}>update-parent</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// parent-render
// child-render
// child-getDerivedStateFromProps
// nextProps { n: 1 }
// child-componentDidUpdate
// nowState { num: 1 }
点击父组件的更新按钮钩子的执行顺序如下:parent-render
→ child-render
→ child-getDerivedStateFromProps
→ child-componentDidUpdate
。
getDerivedStateFromProps
除了上面叙述的用法的注意事项,与componentWillReceiveProps
相比还有两个优势:
- 第一点是默认第一次渲染时也会执行该 “钩子”;
- 第二点是不需要再通过调用
setState
将新的props
转换成组件的状态,可以直接通过返回值设置状态。
getSnapshotBeforeUpdate 钩子
getSnapshotBeforeUpdate
“钩子” 用于替代 componentWillUpdate
“钩子”,不能与 componentWillMount
“钩子” 同时使用,必须与 componentDidUpdate
“钩子” 同时使用,需返回一个值或者 null
,该值会传给 componentDidUpdate
“钩子” 的第三个参数。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
// 点击事件
handleClick = () => {
this.setState({ num: this.state.num + 1 });
}
getSnapshotBeforeUpdate() {
console.log('getSnapshotBeforeUpdate');
return 123;
}
componentDidUpdate(prevProps, prevState, prop) {
console.log('componentDidUpdate');
console.log('prop', prop);
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
// render
// getSnapshotBeforeUpdate
// componentDidUpdate
// prop 123
点击更新按钮执行顺序为:render
→ getSnapshotBeforeUpdate
→ componentDidUpdate
。
总结
以上就是关于
React
生命周期的内容,涵盖了在React
开发中对生命周期大部分的应用,也是React
知识体系中非常重要的部分,React
生命周期和Vue
相比的特点是名字长,不容易记,希望大家在学习理解之后多巩固,孰能生巧。