【连载】浅析React生命周期之二:更新阶段

598 阅读5分钟

【连载】浅析React生命周期之一:挂载阶段

二、更新阶段

react的 UI 组件由 props 和 state 驱动,props接受外部输入,state 存放内部状态。

2.1 先说说外部 props 变化所引起的组件更新。

组件初次挂载,依序执行以下方法。

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

为了演示效果,简单拆分为两个嵌套的父子组件App.jsHeader.js.

以下为 App.js

import React, { Component } from 'react';
import './App.css';
import Header from './Header.js';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count:0
    };
    console.log('App:constructor')
  }
  componentWillMount(){
    console.log('App:componentWillMount')
  }
  componentDidMount() {
    console.log('App:componentDidMount')
    console.log('--------------------------------------------')
  }
  //WARNING! To be deprecated in React v17. Use componentDidUpdate instead.
  componentWillUpdate(nextProps, nextState) {
    console.log('App:componentWillUpdate')
  }
  componentDidUpdate(){
    console.log('App:componentDidUpdate')
    console.log('--------------------------------------------')
  }
  componentWillReceiveProps(nextProps) {
    console.log('App:componentWillReceiveProps')
  }
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    // this.setState({});
  }

  render() {
    console.log('App:render', this.state)
    return (
      <div className="App">
        <Header onClick={this.handleClick} count={this.state.count}/>
      </div>
    );
  }
}

export default App;

以下为 Header.js

import React, { Component, PureComponent } from 'react';
import logo from './logo.svg';
import './Header.css';

class Header extends Component {
  constructor(props) {
    super(props);
    console.log('\tHeader:constructor')
  }
  componentWillMount(){
    console.log('\tHeader:componentWillMount')
  }
  componentDidMount() {
    console.log('\tHeader:componentDidMount')
  }
  //WARNING! To be deprecated in React v17. Use componentDidUpdate instead.
  componentWillUpdate(nextProps, nextState) {
    console.log('\tHeader:componentWillUpdate')
  }
  componentDidUpdate(){
    console.log('\tHeader:componentDidUpdate')
  }
  componentWillReceiveProps(nextProps) {
    console.log('\tHeader:componentWillReceiveProps')
  }

  handleClick = () => {
    this.props.onClick()
  }

  render() {
    console.log('\tHeader:render', this.props)
    return (
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
          <div>{this.props.count}次</div>
          <button onClick={this.handleClick}>+1</button>
        </header>
    );
  }
}

export default Header;

初始化日志如下,可以清楚得看出父子组件的挂载阶段的钩子函数执行情况:

点击+1按钮,观察日志输出,可以看出外部App 父组件 state.count加1后,Header的 props接收到了 props 的变化的执行情况。

以上情况很正常,那么如果外部的 handleClick 中不改变 state 的 count 会怎么样呢?子组件会执行更新阶段吗?我们来试一下。

仅App.js的 handleClick 变更和输出日子如下:

  handleClick = () => {
    /*this.setState({ count: this.state.count + 1 });*/
    this.setState({});
  }

从上面可以看出,只要父组件的 render 执行了,子组件就会执行更新阶段。对性能稍微有点要求的场合,及某些需要严格控制父子组件更新阶段的场景,我们可能需要限制这种情况的发生。

那么,我们应该怎么做了。对了,就是上文还未提及的 shouldComponentUpdate。我们可以在子组件 Header 的这个钩子函数中比较 state 和 props 是否变化,只当变化了我们才允许 Header执行 update。

Header.js添加如下钩子函数,及输出日子如下:

  shouldComponentUpdate(nextProps, nextState) {
    if(nextProps.count !== this.props.count){
      return true;
    }
    return false;
  }

可以看出,子组件没有再执行多余的componentWillUpdate、render、componentDidUpdate。

其实,为了简化这种场景的处理,在 react@15.3.0 (July 29, 2016)这个版本中,已经新增了一个 PureComponent 组件。使用这个类不需要自己实现shouldComponentUpdate。以下是改用这个组件类的情况。

Header.js

import React, { PureComponent } from 'react';
import logo from './logo.svg';
import './Header.css';

class Header extends PureComponent {
  constructor(props) {
    super(props);
    console.log('\tHeader:constructor')
  }
  componentWillMount(){
    console.log('\tHeader:componentWillMount')
  }
  componentDidMount() {
    console.log('\tHeader:componentDidMount')
  }
  //WARNING! To be deprecated in React v17. Use componentDidUpdate instead.
  componentWillUpdate(nextProps, nextState) {
    console.log('\tHeader:componentWillUpdate')
  }
  componentDidUpdate(){
    console.log('\tHeader:componentDidUpdate')
  }
  componentWillReceiveProps(nextProps) {
    console.log('\tHeader:componentWillReceiveProps')
  }

  handleClick = () => {
    this.props.onClick()
  }

  render() {
    console.log('\tHeader:render', this.props)
    return (
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
          <div>{this.props.count}次</div>
          <button onClick={this.handleClick}>+1</button>
        </header>
    );
  }
}

export default Header;

可以看出,我们把 shouldComponentUpdate 去掉后,将 extends Component 改成了 extends PureComponent,就实现了之前的效果。很简洁✌ ️

2.2 再说说内部 state 变化所引起的组件更新。

内部 state 变化引起的组件更新跟外部 props 引起的子组件更新相似,只是内部 state 的变化不会触发componentWillReceiveProps的执行。

2.3 注意:在componentWillUpdate 中执行 setState所引起的效果,跟在 componenetWillMount 有所区别。

【连载】浅析React生命周期之一:挂载阶段 这篇文章中提到过 在componenetWillMount中 setState,新的state 不会引起新的更新行为,但是新的 state内容会被带到 render 中体现。但是在componentWillUpdate则相反,它的下一步本来就是 render,而且新的 state 内容不会被带到 render 中。如果在componentWillUpdate确实设置了新的不同的 state,则会引起循环的更新行为,如果只是调用了 setState,但是 state 内容并无变化,则不会引起循环的渲染更新行为。

- componentWillUpdate
- render
- componentDidUpdate
    - componentWillUpdate
    - render
    - componentDidUpdate
        - componentWillUpdate
        - render
        - componentDidUpdate
...
...

这么说来,好像有点抽象和模糊,举个例子 🌰。Header 组件改造如下:

import React, { PureComponent } from 'react';
import logo from './logo.svg';
import './Header.css';

class Header extends PureComponent {
  constructor(props) {
    super(props);
    console.log('\tHeader:constructor')
  }
  componentWillMount(){
    console.log('\tHeader:componentWillMount')
  }
  componentDidMount() {
    console.log('\tHeader:componentDidMount')
  }
  //WARNING! To be deprecated in React v17. Use componentDidUpdate instead.
  componentWillUpdate(nextProps, nextState) {
    console.log('\tHeader:componentWillUpdate',this.state, nextState)
    const { c = 0 } = nextState || {};
    this.setState({ c: c + 1 });
  }
  componentDidUpdate(prevProps, prevState) {
    console.log('\tHeader:componentDidUpdate', this.state,prevState)
  }
  componentWillReceiveProps(nextProps) {
    console.log('\tHeader:componentWillReceiveProps')
  }

  handleClick = () => {
    // this.props.onClick()
    const { c = 0 } = this.state || {};
    this.setState({ c: c + 1 });
  }

  render() {
    // console.log('\tHeader:render', this.props)
    console.log('\tHeader:render', this.state)
    return (
      <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
          <div>{this.props.count}次</div>
          <button onClick={this.handleClick}>+1</button>
        </header>
    );
  }
}

export default Header;

那么,是不是就是说我们不能在 componentWillUpdate 中 setState呢?很多react技术文章中都这样简单绝对得一笔带过,不能!但是。。。现实业务场景是复杂的,有时我们确实需要在 componentWillUpdate 中更新 dom,然后根据新的 dom 再次调整 dom,以达到最终的展示效果。

其实,如果把 state 控制得当,当然可以在 componentWillUpdate 中 setState。不然,react 库干脆内部直接报错奔溃得了,为何还要允许我们去 setState 一下,然后多余得通过循环调用堆栈溢出的方式告知我们不能这么做?!对吧。

让我们来尝试一下,将上面的Header.js 的 componentWillUpdate 更改为如下:

  componentWillUpdate(nextProps, nextState) {
    const SOME_VAL_DEPEND_ON_DOM = 100;
    console.log('\tHeader:componentWillUpdate',this.state, nextState)
    const { c = 0 } = nextState || {};
    if(c < SOME_VAL_DEPEND_ON_DOM){
      setTimeout(() => {
        this.setState({ c: c + 1 });
      },100);
    }else{
      console.log('\tadjust update finish!')
    }
  }

注意到,这次即使 componentWillUpdate 循环次数远远超过了 react 的限制,应用也没有报错奔溃了。