前言

本篇文章主要内容针对 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 方法

constructorES6 中类的写法中给实例设置属性的钩子,在类的实例被创建时执行,下面是对比 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

执行顺序:constructorcomponentWillMountrendercomponentDidMount

componentWillUpdate 钩子

在调用 setState 更新数据后会触发 render 钩子对组件重新渲染,在执行 render 前会调用 componentWillUpdate 钩子,即将要更新时执行(此时状态和页面都没更新),钩子默认有三个参数,分别为 nextPropsnextStatenextContext,即更新后的属性对象、状态对象和上下文对象。

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 状态和页面都已经更新,钩子默认有三个参数,分别为 prevPropsprevStateprevContext,即更新前的属性对象、状态对象和上下文对象。

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 {}

触发点击事件后的执行顺序为:componentWillUpdaterendercomponentDidUpdate

shouldComponentUpdate 钩子

在使用 setState 更改状态时,其实还会默默的执行 shouldComponentUpdate “钩子”,该钩子有返回值,不使用该 “钩子” 的情况下默认返回值为 true,若使用该 “钩子” 必须指定布尔类型的返回值 truefalse,当返回值为 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 时,触发点击事件后的执行顺序为:shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate

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 “钩子” 会在组件卸载之前触发,卸载组件需调用 ReactDOMunmountComponentAtNode 方法,并传入一个根节点,将会卸载这个根节点内部的所有组件。

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-componentWillUpdateparent-renderchild-componentWillUpdateclild-renderchild-componentDidUpdateparent-componentDidUpdate

点击子组件更新按钮生命周期的执行顺序:child-componentWillUpdateclild-renderchild-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-componentWillUpdateparent-renderchild-componentWillReceivePropschild-shouldComponentUpdatechild-componentWillUpdatechild-renderchild-componentDidUpdateparent-componentDidUpdate

由此可以说明 componentWillReceiveProps 钩子在 shouldComponentUpdate 之前执行。

componentWillReceiveProps “钩子” 在第一次渲染父子组件时不执行,在 React 16.x 版本中被标记为 “已废弃”。

关于 setState 在生命周期中的使用

React 生命周期 “钩子” 中,只有 componentWillMountcomponentDidMountcomponentWillReceiveProps 中可以调用 setState

原因是 setState 方法会触发 render “钩子” 执行,而 shouldComponentUpdatecomponentWillUpdatecomponentDidUpdate 是在 render 后触发,包括在 render 中调用 setState,都会出现更新 “死循环” 的现象,最后造成堆栈溢出,而 componentWillUnmount “钩子” 执行时,组件将被卸载,在此时更新状态毫无意义。

componentWillReceiveProps 中使用 setState,其目的是为了将新更改的属性更新为该组件的状态,但 React 官方不建议这样使用。

React 生命周期流程图

下面是一张关于目前版本比较常用的 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-renderchild-renderchild-getDerivedStateFromPropschild-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

点击更新按钮执行顺序为:rendergetSnapshotBeforeUpdatecomponentDidUpdate

总结

以上就是关于 React 生命周期的内容,涵盖了在 React 开发中对生命周期大部分的应用,也是 React 知识体系中非常重要的部分,React 生命周期和 Vue 相比的特点是名字长,不容易记,希望大家在学习理解之后多巩固,孰能生巧。