日常抄書之一次性搞懂解React生命週期

不想當架構的前端不是好廚子發表於2019-03-14

1. 初步瞭解React生命週期

React生命週期可以分為掛載、更新、解除安裝三個階段。主要可以分為兩類:

  • 元件掛載和解除安裝;
  • 元件接收新的資料和狀態時的更新;

1.1 元件的掛載

元件的掛載是最基本過程,這個過程主要做初始化。在這初始化個過程中componentWillMount會在render方法之前執行,而componentDidMount方法會在render方法之後執行。分別代表了渲染前後時刻。寫一個簡單的例子:

class Demo extends React.Component {
    static propTypes = {}
    static defaultProps = {}
    constructor(props) {
        super(props)
        this.state = {}
    }
    componentWillMount() {}
    render() {return null}
    componentDidMount() {}
}
複製程式碼

如上,這個初始化過程沒有什麼特別之處,這裡包括讀取初始state讀取初始props、以及兩個生命週期方法componentWillMountcomponentDidMount。這些都只會在元件初始化時執行一次

1.2 元件的解除安裝

元件的解除安裝只有componentWillUnmount這個一個方法。

1.3 元件的更新

元件的更新發生在父元件傳遞props或者自身執行setState改變狀態這一系列操作的情況下。和元件更新的生命週期方法有以下幾個:

class Demo extends React.Component {
    //當元件更新時會順序執行以下方法
    componentWillReceiveProps(nextProps){} //
    shouldComponentUpdate(nextProps, nextState) {} //返回false則停止向下執行,預設返回true
    componentWillUpdate(nextProps, nextState) {}
    render() {}
    componentDidUpdate(prevProps, prevState) {}
}
複製程式碼

tip: shouldComponentUpdate可以用來正確的渲染元件的。理想情況下,父級節點改變時,只會重新渲染一條鏈路上和該props相關的元件。但是預設情況下,React會渲染所有的節點,因為shouldComponentUpdate預設返回true。

日常抄書之一次性搞懂解React生命週期

2. 深入瞭解React生命週期

前面大致介紹了元件的生命週期主要分為三種狀態:掛載、更新、解除安裝。如下圖可以詳細瞭解不同狀態的執行順序:

日常抄書之一次性搞懂解React生命週期

使用ES6 classes構建元件的時候static defaultProps={}其實就是呼叫內部的getDefaultProps方法。constructor中的this.state={}其實就是呼叫內部的getInitialState方法。

2.1 詳解React生命週期

自定義元件生命週期通過3個階段進行控制:MOUNTING,RECEIVE_PROPSUNMOUNTING,它負責通知元件當時所處的階段,應該執行生命週期中的哪個步驟。這三個階段分別對應三個方法:

日常抄書之一次性搞懂解React生命週期

2.2使用createClass建立自定義元件

createClass是建立自定義元件的入口方法,負責管理生命週期中的getDefaultProps方法。該方法在整個生命週期中只執行一次,這樣所有例項初始化的props都能共享。 通過createClass建立自定義元件,利用原型繼承ReactClassComponent父類,按順序合併mixin,設定初始化defaultProps,返回建構函式。

var ReactClass = {
  createClass: function(spec) {
    var Constructor = function(props, context, updater) {
  
      // 自動繫結
      if (this.__reactAutoBindPairs.length) {
        bindAutoBindMethods(this);
      }
  
      this.props = props;
      this.context = context;
      this.refs = emptyObject;
      this.updater = updater || ReactNoopUpdateQueue;
  
      this.state = null;
  
      //ReactClasses沒有建構函式,通過getInitialState和componentWillMount來代替
      var initialState = this.getInitialState ? this.getInitialState() : null;
      this.state = initialState;
    };

    //原型繼承ReactClassComponent父類
    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];
  
    //合併mixin
    injectedMixins.forEach(
      mixSpecIntoComponent.bind(null, Constructor)
    );
  
    mixSpecIntoComponent(Constructor, spec);
  
    //所有mixin合併後初始化defaultProps(在生個生命週期中,defaultProps只執行一次)
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }
    //設定原型
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }
     //最後返回的是建構函式
    return Constructor;
  },
}
複製程式碼

2.3 階段一:MOUNTING

mountComponent負責管理生命週期中的getInitialState,componentWillMount,rendercomponentDidMount。 由於getDefaultProps是在初始化建構函式中進行管理的,所以也是整個生命週期中最先執行的。而且只執行一次也可以理解了。

由於通過ReactCompositeComponentBase返回的是一個虛擬節點,所以需要通過** instantiate-ReactComponent去得到例項,在通過mountComponent**拿到結果作為當前自定義元素的結果。

通過mountComponent掛載元件,初始化序號,標記引數等,判斷是否為無狀態元件,並進行對應的初始化操作,比如初始化props,context等引數。利用getInitialState獲取初始化state, 初始化更新佇列和更新狀態。

如果存在componentWillMount則執行,如果此時在componetWillMount呼叫setState方法,是不會觸發re-render方法,而是會進行state合併,且inst.state = this._processPendingState(inst.props, inst.context)componentWillMount之後執行。因此在render中才可以獲取到最新的state。

因此,React是通過更新佇列this._pendingStateQueue以及更新狀態this._pendingReeplaceStatethis._pendingForUpdate來實現setState的非同步更新。

當渲染完成後,若存在componentDidMount則呼叫。

其實mountComponent是通過遞迴渲染內容。由於遞迴的特性,父元件的componentWillMount在其子元件的componentWillMount之前呼叫,父元件的componentDidMount在其子元件的componentDidMount之後呼叫。

日常抄書之一次性搞懂解React生命週期

//react/src/renderers/shared/reconciler/ReactCompositeComponent.js

//當元件掛載時,會分配一個遞增編號,表示執行ReactUpdates時更新元件的順序
var nextMountID = 1
var ReactCompositeComponentMixin = {
  //初始化元件,渲染標記,註冊事件監聽器
  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
    this._context = context; //當前元件對應的上下文
    this._mountOrder = nextMountID++;
    this._nativeParent = nativeParent;
    this._nativeContainerInfo = nativeContainerInfo;

    var publicProps = this._processProps(this._currentElement.props);
    var publicContext = this._processContext(context);

    var Component = this._currentElement.type;

    // 初始化公共類
    var inst;
    var renderedElement;

    //這裡判斷是否是無狀態元件,無狀態元件沒有更新狀態序列,只關注更新
    if (Component.prototype && Component.prototype.isReactComponent) {
      inst = new Component(publicProps, publicContext, ReactUpdateQueue);
    } else {
      inst = Component(publicProps, publicContext, ReactUpdateQueue);
      if (inst == null || inst.render == null) {
        renderedElement = inst;
        warnIfInvalidElement(Component, renderedElement);
        invariant(
          inst === null ||
          inst === false ||
          ReactElement.isValidElement(inst),
          '%s(...): A valid React element (or null) must be returned. You may have ' +
          'returned undefined, an array or some other invalid object.',
          Component.displayName || Component.name || 'Component'
        );
        inst = new StatelessComponent(Component);
      }
    }



    // These should be set up in the constructor, but as a convenience for
    // simpler class abstractions, we set them up after the fact.
    //這些初始化引數應該在建構函式中設定,再此處設定為了便於簡單的類抽象
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = ReactUpdateQueue;

    this._instance = inst;

    // 將例項儲存為一個引用
    ReactInstanceMap.set(inst, this);

    //初始化state
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }

    //初始化state更新佇列
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    var markup;
    //如果掛載錯誤則執行performInitialMountWithErrorHandling(方法如下)
    if (inst.unstable_handleError) {
      markup = this.performInitialMountWithErrorHandling(
        renderedElement,
        nativeParent,
        nativeContainerInfo,
        transaction,
        context
      );
    } else {
      //執行掛載
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }

    //如果存在componentDidMount則呼叫
    if (inst.componentDidMount) {
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }

    return markup;
  },
  //掛載錯誤執行方法
  performInitialMountWithErrorHandling: function (
    renderedElement,
    nativeParent,
    nativeContainerInfo,
    transaction,
    context
  ) {
    var markup;
    var checkpoint = transaction.checkpoint();
    try {
      //如果沒有錯誤則初始化掛載
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } catch (e) {
      // Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
      transaction.rollback(checkpoint);
      this._instance.unstable_handleError(e);
      if (this._pendingStateQueue) {
        this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
      }
      checkpoint = transaction.checkpoint();
      //如果捕捉到錯誤,則執行unmountComponent後再初始化掛載
      this._renderedComponent.unmountComponent(true);
      transaction.rollback(checkpoint);
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }
    return markup;
  },
  //初始化掛載方法
  performInitialMount: function(renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var inst = this._instance;
    //如果存在componentWillMount則呼叫
    if (inst.componentWillMount) {
      inst.componentWillMount();
      //如果在componentWillMount觸發setState時,不會觸發re-render,而是自動提前合併
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }

    // 如果不是無狀態元件則直接渲染
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
    //得到 _currentElement對應的component類例項
    this._renderedComponent = this._instantiateReactComponent(
      renderedElement
    );

    //遞迴渲染
    var markup = ReactReconciler.mountComponent(
      this._renderedComponent,
      transaction,
      nativeParent,
      nativeContainerInfo,
      this._processChildContext(context)
    );

    return markup;
  }
}

複製程式碼

2.4 階段二:REVEIVE_PROPS

updateComponent負責管理生命週期的componentWillReceivePropsshouldComponentcomponentWillUpdaterendercomponentDidUpdate

首先通過updateComponent更新元件,如果前後元素不一致,說明需要元件更新。

若存在componentWillReceiveProps,則執行。如果此時在componentWillReceiveProps中呼叫setState是不會觸發re-render,而是會進行state合併。且在componentWillReceiveProps,shouldComponentUpate和componentWillUpdate是無法獲取更新後的this.state。需要設定inst.state = nextState後才可以。因此只有在render和componentDidUpdate中才可以獲取更新後的state.

呼叫shouldComponentUpdate判斷是否需要進行元件更新,如果存在componentWillUpdate則執行。

updateComponet也是通過遞迴渲染的,由於遞迴的特性,父元件的componentWillUpdate在子元件之前執行,父元件的componentDidUpdate在其子元件之後執行。

日常抄書之一次性搞懂解React生命週期

2.5 階段三:UNMOUNTING

unmountComponent負責管理componentWillUnmount。在這個階段會清空一切。

//元件解除安裝
  unmountComponent: function(safely) {
    if (!this._renderedComponent) {
      return;
    }
    var inst = this._instance;

    //如果存在componentWillUnmount,則呼叫
    if (inst.componentWillUnmount) {
      if (safely) {
        var name = this.getName() + '.componentWillUnmount()';
        ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
      } else {
        inst.componentWillUnmount();
      }
    }

    //如果元件已經渲染,則對元件進行unmountComponent操作
    if (this._renderedComponent) {
      ReactReconciler.unmountComponent(this._renderedComponent, safely);
      this._renderedNodeType = null;
      this._renderedComponent = null;
      this._instance = null;
    }

    //重置相關引數,更新佇列以及更新狀態
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    this._pendingCallbacks = null;
    this._pendingElement = null;
    this._context = null;
    this._rootNodeID = null;
    this._topLevelWrapper = null;
    //清除公共類
    ReactInstanceMap.remove(inst);
  },
複製程式碼

React新特性來了

日常抄書之一次性搞懂解React生命週期
如上圖,這是React16.3出來的新的生命週期圖。並不代表之前的有的生命週期不在了。在React16.x版本中還可以使用之前的生命週期方法。在React17後都將廢除,使用如上圖的生命週期。

如圖,多了getDerivedStaetFromPropsgetSnapShotBeforeUpdate

1.static getDerivedStateFromProps(nextProps, prevState)

getDerivedStateFromProps會在呼叫render之前呼叫,並且在初始掛載以及後續更新時都會被呼叫。他應該返回一個物件來更新state。如果返回null則不更新任何內容。它接收兩個引數,一個是傳進來的nextProps和之前的prevState。

2. getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate在最近一次渲染輸出(提交至DOM節點)之前呼叫,它使得元件能在發生更改之前從DOM中捕獲一些資訊。此元件返回的任何值將作為componentDidUpdate的第三個引數。

3. static getDerivedStateFromError(error)

此生命週期會在後代元件丟擲錯誤後被呼叫。它將丟擲的錯誤作為引數,並返回一個值以更新 state。

4. componentDidCatch(error, info)

此生命週期在後代元件丟擲錯誤後被呼叫。 它接收兩個引數: error —— 丟擲的錯誤。 info —— 帶有 componentStack key 的物件,其中包含有關元件引發錯誤的棧資訊。

相關文章