一篇读懂React生命周期

1,044 阅读7分钟

组件实例从被创建到被销毁的过程叫做组件的生命周期。在React中只有Class组件拥有生命周期,因为只有Class组件才有相应的实例。

当Class组件实例被创建并插入DOM中时会进入到生命周期的挂载(Mount) 状态,当组件的props或state改变时会进入到更新(Update) 状态,当组件从DOM中移除时会进入到卸载(Unmount) 状态,当渲染过程、生命周期或子组件的构造函数中抛出错误时会进入到错误处理(Error) 状态。

组件的生命周期可以分为三个阶段:render阶段pre-commit阶段commit阶段。render阶段是纯净不含副作用的,可以被暂停、中止或重新启动;pre-commit阶段可以读取DOM;commit阶段可以使用DOM,运行副作用,安排更新。

未命名文件 (1).png 注:红色的为已经废弃的生命周期,绿色的为新增的生命周期。

生命周期钩子

下面为大家介绍这些阶段中生命周期钩子的调用时机以及它们的作用。

constructor

该方法只会执行一次,一般我们会在组件挂载之前调用该方法为组件进行初始化内部state为事件处理函数绑定实例的操作。

在该方法中我们不需要调用setState方法,而是可以直接用this.state对state进行初始化赋值。

componentWillReceiveProps

该方法在已挂载组件接收到新的 props 之前被调用。你可以根据新旧 props使用this.state来更新 state。

在首次渲染组件时,不会调用此生命周期钩子,只会在props更新时调用。而且使用 this.setState 触发组件更新时,也不会调用此生命周期钩子。不过,如果是父组件渲染导致了组件的重新渲染,即使传给该组件的 props 没变,该组件中的这个生命周期函数也会被调用。

我们一般不使用此生命周期函数,因为它会出现出现 bug 和不一致性。

static getDerivedStateFromProps

它是一个静态方法,正如它的名字所写,它可以根据props的值来决定state的值。它接收props和state两个参数,返回一个对象来更新state,如果返回null则不更新。

该方法在每次调用render前都会被调用,无论是挂载时还是更新时都会被调用。因此它可以作为componentWillMount、componentWillUpdate 和 componentWillReceiveProps 的替代方案。

另外,由于它是一个静态的方法,所以无权访问组件实例, 如果在内部使用this访问到的也不是组件实例。

这个生命周期钩子我们不常使用,因为派生状态不仅会导致代码冗余,而且使组件难以维护,我们也不推荐使用。

shouldComponentUpdate

该方法在首次渲染或者使用forceUpdate函数时该方法不会被调用,如果props或者state发生变化,该方法会在render方法执行之前被调用,默认返回值为true,返回false则不会执行render方法即不会更新组件,但要注意的是它不会阻止子组件因为state改变而导致的更新。

它会接收nextProps和nextState两个参数,通过比较新旧props和新旧state来决定是否返回false来阻止组件的更新。

我们往往使用它来优化性能

class Title extends React.Component {
    constructor() {
        this.state = {
            name:'标题',
        }
    }
    render() {
        return (
            <div className="title">
                <h1>{this.state.name}</h1>
            </div>
        )
    }
}
​
class Content extends React.Component{
    constructor(){
        this.state = {
            content:'内容',
        }
    }
    
    componentDidMount() {
        this.setState({
            content:'新的内容',
        })
    }
    
    render() {
        return (
            <div className="content">
                <Title></Title>
                <p>{this.state.content}</p>
            </div>
        )
    }
}

当我们在Content组件调用setState方法改变state时,Content组件包括其所有的Title组件都会重新渲染,但是Title组件里并没有state的改变,我不希望它重新渲染。这时候我们可以将Title组件改为这样:

class Title extends React.Component {
    constructor() {
        this.state = {
            name:'标题',
        }
    }
    
    shouldComponentUpdate(nextProps,nextState) {
        return nextState.name !== this.state.name;
    }
    
    render() {
        return (
            <div className="title">
                <h1>{this.state.name}</h1>
            </div>
        )
    }
}

componentWillMount

它在render之前调用,而且只会被调用一次,所以在该方法中调用this.state不会有额外的渲染。

一般我们会在constructor中初始化state,在componentDidMount中引入副作用或者订阅内容,所以这个生命周期钩子不常使用。

componentWillUpdate

该方法和componentWillMount方法类似,只不过它是在组件更新时执行render之前被调用,而componentWillMount是在首次渲染前被调用。另外,如果shouldComponentUpdate方法返回false,那么该方法不会被调用。

同componentWillMount方法一样,我们不常使用此生命周期钩子。

render

组件中唯一一个必须要实现的方法,是一个纯函数,即对于相同的props和state,它总是返回相同的结果,而且它的返回的结果将作为渲染的页面内容。

render方法被调用时,它会返回一下类型之一:

  • React元素:通常为JSX语法,比如<div /><MyComponent />等。
  • 数组或者Fragments:通过数组返回多个元素。
  • Portals:渲染子节点到不同的子树中。
  • 字符串或者数值类型:会被作为文本节点渲染。
  • 布尔型或者null:什么都不渲染。

componentDidMount

该方法在组件插入DOM树中立即被调用,如果在方法里直接调用this.setState,那么render方法会再一次被调用,两次渲染都发生在浏览器更新屏幕之前,所以不会影响用户体验,但这会影响性能,所以不推荐这样使用。

依赖于 DOM 节点的初始化操作应该放在这里,另外,我们可以在方法中发送网络请求添加订阅

getSnapshotBeforeUpdate

该方法在渲染提交到DOM树之前被调用,此时DOM树还没有更新,我们可以在这里获得未改变的DOM信息例如DOM的滚动位置。

它接收prevProps和prevState两个参数,它的返回值会作为componentDidUpdate方法的第三个参数。

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>
    );
  }
}

componentDidUpdate

该方法会在DOM更新后立即被调用,首次渲染时不会调用该方法。我们可以在这个方法中直接调用this.setState,但是必须包裹在一个条件语句中,否则会导致死循环。

它接收三个参数,分别是 prevProps、prevState、snapshot,即:前一个状态的 props,前一个状态的 state、getSnapshotBeforeUpdate 的返回值。

我们在这里可以对 DOM 进行操作或者进行网络请求

componentWillUnmount

该方法在组件被销毁及被卸载之前调用,组件实例卸载后不会再挂载,所以不应该使用this.setState

我们一般会在这里清除timer取消网络请求或者清除订阅等。

父子组件的生命周期

父子组件初始化

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    console.log("App constructor");
  }
​
  static getDerivedStateFromProps() {
    console.log("App static getDerivedStateFromProps");
    return null;
  }
​
  componentDidMount() {
    console.log("App componentDidMount");
  }
​
  render() {
    console.log("App render");
    return (
      <div>
        <Child />
      </div>
    );
  }
}
​
​
export default class Child extends Component {
  constructor(props) {
    super(props);
    this.state = {
      childCount: 0,
    };
    console.log('Child1 constructor');
  }
​
  static getDerivedStateFromProps(props) {
    console.log('Child1 static getDerivedStateFromProps');
    return null;
  }
​
  componentDidMount() {
    console.log('Child1 componentDidMount');
  }
​
  render() {
    console.log('Child1 render');
    return <div>Child{this.props.order}</div>;
  }
}

其执行结果如下图:

1666768311081.png

由此可以得出结论:在父子组件初始化时,会按以下顺序执行生命周期函数。

  • 父组件 constructor
  • 父组件 getDerivedStateFromProps
  • 父组件 render
  • 子组件 constructor
  • 子组件 getDerivedStateFromProps
  • 子组件 render
  • 子组件 componentDidMount
  • 父组件 componentDidMount

子组件修改state

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    console.log("App constructor");
  }
​
  static getDerivedStateFromProps() {
    console.log("App static getDerivedStateFromProps");
    return null;
  }
​
  getSnapshotBeforeUpdate(prevProps) {
    console.log("App getSnapshotBeforeUpdate");
    return null;
  }
    
  shouldComponentUpdate(nextProps) {
    console.log("App shouldComponentUpdate");
    return true;
  }
​
  componentDidUpdate() {
    console.log("App componentDidUpdate");
  }
​
  componentDidMount() {
    console.log("App componentDidMount");
  }
​
  render() {
    console.log("App render");
    return (
      <div>
        <Child order={1} />
      </div>
    );
  }
}
​
export default class Child extends Component {
  constructor(props) {
    super(props);
    this.state = {
      childCount: 0,
    };
    console.log('Child1 constructor');
  }
​
  static getDerivedStateFromProps(props) {
    console.log('Child1 static getDerivedStateFromProps');
    return null;
  }
​
  shouldComponentUpdate(nextProps) {
    console.log('Child1 shouldComponentUpdate');
    return true;
  }
​
  getSnapshotBeforeUpdate(prevProps) {
    console.log("Child1 getSnapshotBeforeUpdate");
    return null;
  }
  componentDidUpdate() {
    console.log('Child1 componentDidUpdate');
  }
​
  componentDidMount() {
    console.log('Child1 componentDidMount');
  }
​
  render() {
    console.log('Child1 render');
    return (
      <div
        onClick={() =>
          this.setState((childCount) => ({ childCount: childCount++ }))
        }
      >
        Child{this.props.order}
      </div>
    );
  }
}

其执行结果如下图所示:

1666770123443.png

由此可以得出结论:在子组件的state改变时,会按以下顺序执行生命周期函数。

  • 子组件 getDerivedStateFromProps
  • 子组件 shouldComponentUpdate
  • 子组件 render
  • 子组件 getSnapShotBeforeUpdate
  • 子组件 componentDidUpdate

父组件修改state

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    console.log("App constructor");
  }
​
  static getDerivedStateFromProps() {
    console.log("App static getDerivedStateFromProps");
    return null;
  }
​
  shouldComponentUpdate(nextProps) {
    console.log("App shouldComponentUpdate");
    return true;
  }
  getSnapshotBeforeUpdate(prevProps) {
    console.log("App getSnapshotBeforeUpdate");
    return null;
  }
​
  componentDidUpdate() {
    console.log("App componentDidUpdate");
  }
​
  componentDidMount() {
    console.log("App componentDidMount");
  }
​
  render() {
    console.log("App render");
    return (
      <div>
        <div onClick={() => this.setState((count) => ({ count: count + 1 }))}>
          App
        </div>
        <Child order={1} />
      </div>
    );
  }
}
​
export default class Child extends Component {
  constructor(props) {
    super(props);
    this.state = {
      childCount: 0,
    };
    console.log('Child1 constructor');
  }
​
  static getDerivedStateFromProps(props) {
    console.log('Child1 static getDerivedStateFromProps');
    return null;
  }
​
  shouldComponentUpdate(nextProps) {
    console.log('Child1 shouldComponentUpdate');
    return true;
  }
  getSnapshotBeforeUpdate(prevProps) {
    console.log("Child1 getSnapshotBeforeUpdate");
    return null;
  }
​
  componentDidUpdate() {
    console.log('Child1 componentDidUpdate');
  }
​
  componentDidMount() {
    console.log('Child1 componentDidMount');
  }
​
  render() {
    console.log('Child1 render');
    return (
      <div
        onClick={() =>
          this.setState((childCount) => ({ childCount: childCount++ }))
        }
      >
        Child{this.props.order}
      </div>
    );
  }
}

其执行结果如下图所示:

1666770185857.png

由此可以得出结论:在父组件的state改变时,会按以下顺序执行生命周期函数。

  • 父组件 getDerivedStateFromProps
  • 父组件 shouldComponentUpdate
  • 父组件 render
  • 子组件 getDerivedStateFromProps
  • 子组件 shouldComponentUpdate
  • 子组件 render
  • 子组件 getSnapShotBeforeUpdate
  • 父组件 getSnapShotBeforeUpdate
  • 子组件 componentDidUpdate
  • 父组件 componentDidUpdate

参考资料

[React组件的生命周期]  zh-hans.reactjs.org/docs/react-… 

[React生命周期详解]  juejin.cn/post/709613… 

[详解react生命周期和在父子组件中的执行顺序​]  blog.csdn.net/Dax1_/artic… 

[React:完整的生命周期及方法​]  juejin.cn/post/696618…