laizimo / zimo-article

:books:博客——源于实践,乐于分享,欢迎Star~
1.06k stars 95 forks source link

react生命周期的艺术【缺图】 #16

Open laizimo opened 6 years ago

laizimo commented 6 years ago

前言

本篇讲述的是react的生命周期的管理的知识点整理。大体的内容都来自于《深入react技术栈》的生命周期部分。不得不说,这是我看过讲解react源码的最好的一本教科书。生命周期的机制在现在大部分框架中都存在,并且这种机制有着它独有的好处。

正文

首先,我们来了解一下生命周期是什么,表现出怎样的特质。从react的角度,react的生命周期钩子主要有:componentWillMount、render、componentDidMount、componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、componentDidUpdate、componentWillUnmount这样8个。每个生命周期对应着各自的状态,整个生命周期的运作更像是状态机的运作——在不同的阶段,运行不同的方法。

而react就是通过这样子的方式实现了“生命周期——状态——组件”的过程(通过setState方法实现)。

整个react的组件的生命周期可以分成几种情况去执行:

  1. 组件第一次渲染:getDefaultProps -> getInitialState -> componentWillMount -> render -> componentDidMount
  2. 卸载组件:componentWillUnmount
  3. 组件第二次渲染:getInitialState -> componentWillMount -> render -> componentDidMount
  4. 组件props发生改变:componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
  5. 组件state发生改变(props并未改变):shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

如图所示:

lifecycle

那么,react的生命周期又是通过什么来管理的呢?其实,react的生命周期主要分为三个阶段MOUNTING、RECEIVE_PROPS和UNMOUNTING。在源码中,这三个阶段分别对应着三个函数,分别为mountComponent、updateComponent和unmountComponent。

详细了解这三个阶段之前,可以先来看一下上述执行情况中第一次渲染的时候,这时候组件刚好被生成,react是通过createClass方法来创建自定义组件的。而getDefaultProps方法只执行一次,它是由构造函数来管理的,用来设置初始化defaultProps的。

然后我们再来深入了解这三个阶段分别管理的函数:

阶段一:MOUNTING

mountComponent 负责管理生命周期中的getInitialState、componentWillMount、render和componentDidMount。

通过mountComponent挂载组件,初始化序号、标记等参数,判断是否为无状态组件,并进行对应的组件初始化工作,比如初始化props、context等参数。利用getInitialState获取初始化state、初始化更新队列和更新状态。

注:在componentWillMount中调用setState方法,是不会触发re-render的,而会进行state合并。由于inst.state = this._processPendingState(inst.props, inst.context)是在componentWillMount之后执行的,因此componentWillMount中的this.state并不是最新的,最新的可在render中获取。

mountComponent还有一个特点:通过递归渲染内容,即由父组件开始componentWillMount开始,接着子组件的componentWillMount,之后是子组件的componentDidMount,最后才会到父组件的componentDidMount调用。

执行顺序图:

image

阶段二:RECEIVE_PROPS

updateComponent负责管理生命周期中的componentWillReciveProps、shouldComponentUpdate、componentWillUpdate、render和componentDidUpdate。

通过updateComponent来更新组件,如果前后元素不一致,说明需要更新。

此时,如果组件中存在componentWillReciveProps的话,则执行,但是在这个生命周期中调用setState函数并不会触发re-render,只会进行state的合并。同样的,state的最新状态并不能在componentWillReceiveProps、shouldComponentUpdate和componentWillUpdate中通过this.state得到。因为只有当触发了inst.state = nextState这个语句时,才会更新最新的状态,因此,也只能在render和componentDidUpdate这两个环节取得。

注:setState函数并不能在shouldComponentUpdate和componentWillUpdate中进行使用。因为会产生死循环,使得内存爆炸。大致的原理在接下来的setState机制中会详细解释。

和mountComponent的流程差不多,updateComponent也是递归渲染的。

执行流程如图:

updateComponent

阶段三: UNMOUNTING

这个阶段主要管理的生命周期是componentWillUnmount。

执行componentWillUnmount时,会重置相关参数、更新队列以及更新状态。并且这里使用setState方法是不会造成组件的re-render的。

setState的更新机制

在react中,或许被使用最频繁的就是setState这个API了。或许,你有没有思考过,为什么需要这样一个API呢?既然state是一个对象,为何不直接修改对象中的值呢!这样做不是更加方便吗?在react中,基本是禁止这样子做的。1. 直接修改state不会引起组件的更新 2. 或许这样子做了,会导致后面的组件状态被覆盖。

那么说回setState的更新问题?react有一个公式UI = f(state),这个公式表示只需要改变状态,就可以改变UI界面。试想一下,如果频繁地使用setState,react会频繁的去进行重渲染吗?很显然,这是不被允许发生的。这样的结果会导致react的性能急剧下降。首先来看一个例子:

componentDidMount(){
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
  }

这个例子最后的输出结果都是0, 0, 0。或许,与你试想的1,2,3的结果有很大的不同。这是因为setState的更新机制导致的。在同一个生命周期中,setState会将每一次的操作都放到一个状态队列中,最后对这个状态队列进行一个合并操作。这也就是setState的批量更新——即setState的更新是异步的。

那么,问题来了,有的时候,你必须得这么去做,业务的要求,又该如何呢?那就通过函数的形式,就像Redux一样,具备reducer这种类型的函数,setState本身也推荐使用函数的方式。

this.setState((prevState, props) => {
      count: prevState.count + 1
});

这是比较流行的写法,可以充分的解决异步更新带来的状态不统一的问题。其实这是一种函数式的编程方式,setState接受一个函数,而函数会返回一个新的对象。这样的话,当你需要连续更新时,就可以写一个合并函数,在里面进行操作。

再来看一下另一个例子:

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

    this.state = {
      count: 0
    };
  }

  componentDidMount(){
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    setTimeout(() => {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
    }, 0)
  }

  render(){
    return (
      <div>{this.state.count}</div>
    );
  }
}

这里的输出结果是0、0、2、3。

前面两个0的输出,看懂上面的机制之后,就可以很明白了。那么,后面两个输出又发生了什么呢?为什么会是2,3,而不是2,2呢。

首先,来看一下setState调用栈的逻辑判断:

setstate

这里有个判断的过程,会去判断是否处于批量更新的模式,如果是,则进行批量更新,只render一遍;如果不是,这每次都render。这个判断的源码中涉及到了事务的概念。

setState调用死循环

在上面生命周期阶段调用时,谈及到shouldComponentUpdate和componentWillUpdate方法时,说过在其中调用setState的方法会造成死循环。原因是因为调用setState,会执行enqueueSetState方法,并对partialState以及_pendingstateQueue更新队列进行合并操作,最终通过enqueueUpdate执行state更新。

而performUpdateIfNecessary方法会获取_pendingElement、_pendingStateQueue、_pendingForceUpdate,并调用receiveComponent和updateComponent方法进行组件更新。

在shouldComponentUpdate或componentWillUpdate方法中调用时,this._pendingStateQueue != null,则performUpdateIfNecessary方法就会调用updateComponent方法进行组件更新。这样就形成了死循环。

update

总结

这里总结了大部分的react的生命周期的内容,大部分的内容也是来自于《深入浅出react技术栈》。只是书上还有一部分源码没有贴出来。这篇文章也算是对自己看完这部分内容的总结吧

laizimo commented 6 years ago

Dissecting Reacts lifecycle methods setState() Gate setState: 这个API设计到底怎么样