详解 React 15 生命周期

2,910 阅读6分钟

本文主要讲解 React 15 的生命周期,需要关注的有以下生命周期:

  1. constructor()

  2. componentWillReceiveProps()

  3. shouldComponentUpdate()

  4. componentWillMount()

  5. componentWillUpdate()

  6. componentDidUpdate()

  7. componentDidMount()

  8. render()

  9. componentWillUnmount()

这些生命周期是如何进行串联以及相互依存的呢?

接下来我们会分挂载、更新和卸载三个阶段来对 React 15 的生命周期进行讲解。

挂载阶段(Mounting):组件的初始化渲染

在挂载阶段,组件实例会被创建并插入到 DOM 中。其生命周期调用顺序如下:

代码如下:

import React from "react";

export default class App extends React.Component {
  constructor(props) {
    super(props);
    console.log("constructor()");
  }

  componentWillMount() {
    console.log("componentWillMount()");
  }

  componentDidMount() {
    console.log("componentDidMount()");
  }

  render() {
    console.log("render()");
    return (
      <div>
        <h1>Hello World!</h1>
      </div>
    );
  }
}

constructor()

在组件挂载之前,会调用它的构造函数,只会被调用一次。

通常,在 React 中,构造函数仅用于以下两种情况:

  • 通过给 this.state 赋值对象来初始化内部 state。
  • 为事件处理函数绑定实例
constructor(props) {
  super(props);
  // 不要在这里调用 this.setState()
  this.state = { counter: 0 };
  // 替换做法:使用箭头函数
  this.handleClick = this.handleClick.bind(this);
}

关于 constructor() 的更多介绍请访问官网文档

componentWillMount()

这个方法同样也只会在挂载阶段调用且只会被调用一次。

componentWillMount() 在 render() 之前被调用,因此在此方法中同步 setState() 不会触发额外渲染。通常,建议使用 constructor() 来初始化 state。

避免在此方法中引入任何副作用或订阅。这些操作往往会伴随一些风险或者说不必要性。(具体原因我们将在以后讲解)。如果遇到这种情况,请改用 componentDidMount()。

关于 componentWillMount() 的更多介绍请访问官网文档

render()

render() 的功能主要是 输出需要渲染的内容。需要注意的是 render() 在执行过程中并不会去操作真实 DOM,也就是说不会渲染。真实 DOM 的渲染,是在挂载阶段由 ReactDOM.render 来处理的。

另外重要的一点是 render() 应该为纯函数 ,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果。

关于 render() 的更多介绍请访问官网文档

componentDidMount()

componentDidMount() 在 render() 之后调用,此时真实 DOM 已经挂载到页面上,可以进行与真实 DOM 相关的操作,如网络请求、添加订阅等。

关于 componentDidMount() 的更多介绍请访问官网文档

更新阶段(Updating):组件的更新

触发组件更新的方式有三种:

  1. 新的 props
  2. setState()
  3. forceUpdate() 而 forceUpdate() 官方不推荐使用且应该避免使用。所以组件的更新可以分为两种:
  • 由父组件更新触发的更新 => 新的 props
  • 组件自身触发的更新 => setState()

对应的生命周期流程如下图所示:

componentWillReceiveProps()

componentWillReceiveProps(nextProps);

此方法中,nextProps 表示的是新的 props,而现有的 props 可以通过 this.props 获取,从而可以比较两者的不同。

那么 componentWillReceiveProps() 什么时候会被触发呢?根据方法命名,认为 componentWillReceiveProps 是在组件的 props 内容发生了变化时被触发的。这种说法正确与否,揭晓前先看个示例。

import React from "react";

class Child extends React.Component {
  constructor(props) {
    super(props);
  }

  componentWillReceiveProps() {
    console.log("componentWillReceiveProps");
  }

  shouldComponentUpdate() {
    console.log("shouldComponentUpdate");
    return true;
  }

  componentWillUpdate() {
    console.log("componentWillUpdate");
  }

  componentDidUpdate() {
    console.log("componentDidUpdate");
  }

  render() {
    console.log("render");
    return <div>随机数为:{this.props.count}</div>;
  }
}

export default class App extends React.Component {
  state = {
    count: 0
  };

  handleClick = () => {
    this.setState({
      count: Math.round(Math.random() * 10)
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>随机数</button>
        <Child count={this.state.count} />
      </div>
    );
  }
}

展示如图所示:

通过点击“随机数”按钮,来修改传入到 Child 组件中的 count 值。当 count 值发生改变时,此时一定会触发 componentWillReceiveProps()。生命周期方法调用顺序如下:

但如果我们在父组件中添加一个与 Child 组件无关的一个状态,改变这个状态后,会如何变化呢? 修改后的代码如下:

// 子组件代码没有改变

export default class App extends React.Component {
  state = {
    count: 0,
    clickCount: 0
  };

  handleClick = () => {
    this.setState({
      count: Math.round(Math.random() * 10)
    });
  };

  handleIncrement = () => {
    this.setState({
      clickCount: this.state.clickCount + 1
    });
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>随机数</button>
        <button onClick={this.handleIncrement}>加1</button>
        <div>与子组件无关的点击次数:{this.state.clickCount}</div>
        <Child count={this.state.count} />
      </div>
    );
  }
}

修改后的页面:

当点击“加1”按钮后,会发生什么呢?

可以看到,componentWillReceiveProps() 依然被触发。

官方文档中提到:

如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较。

通过示例和官方文档可以得出一个结论:

componentWillReceiveProps() 并不是由 props 的变化触发的,而是由父组件的更新触发的

shouldComponentUpdate()

shouloadComponentUpdate(nextProps, nextState)

shouldComponentUpdate() 通常会作为性能优化的手段,根据其返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。

如果返回值为 false,则后续生命周期函数不会调用:componentWillUpdate()、render() 和 componentDidUpdate()。

我们也可以使用官方提供的 PureComponent。PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

componentWillUpdate()

我们可以在此方法中执行一些不涉及真实 DOM 操作的工作。

componentDidUpdate()

componentDidUpdate(prevProps, prevState)

在组件更新完毕后被触发,和 componentDidMount() 用法类似,可以在此处对 DOM 进行操作。同样,也可以进行网络请求,但要主要比较更新前后的 props 值。

官方示例:

componentDidUpdate(prevProps) {
	// 典型用法(不要比较 props):
	if (this.props.userId !== this.props.userID) {
		this.fetchData(this.props.userID);
	}
}

关于 componentDidUpdate() 的更多介绍请访问官网文档

卸载阶段(Unmounting):组件的卸载

卸载阶段就比较简单,只与一个生命周期有关:componentWillUnmount()。

componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前调用。在此方法中可以执行清理操作,比如 清除定时器 timer,取消网络请求、清除在 componentDidMount() 中创建的订阅以及重置状态等。

在此方法中不应该调用 setState(),因为该组件卸载后,将永远不会重新渲染。

总结

本篇文章中我们从 React 组件的三个阶段出发,分别介绍了各自阶段涉及到的生命周期和其用法。相信大家已经对 React 15 的生命周期有了清晰的认识。

而在 React 16 中,组件的生命周期发生了一些变化,有些不再推荐使用,又增加了一些新的生命周期。

下篇文章中,将会详细介绍 React 16的生命周期。通过对比两者,解答出以下问题:

  • 为什么 React 15 中的有些生命周期被官方标注为 UNSAFE?
  • React 16 中新增的这些生命周期有什么考量?

如有错误,请指正。

参考