1. 程式人生 > >React原始碼分析4 — React生命週期詳解

React原始碼分析4 — React生命週期詳解

1 React生命週期流程

Markdown

呼叫流程可以參看上圖。分為例項化,存在期和銷燬三個不同階段。介紹生命週期流程的文章很多,相信大部分同學也有所瞭解,我們就不詳細分析了。很多同學肯定有疑問,這些方法在React內部是在哪些方法中被呼叫的呢,他們觸發的時機又是什麼時候呢。下面我們來詳細分析。

2 例項化生命週期

getDefaultProps

在React.creatClass()初始化元件類時,會呼叫getDefaultProps(),將返回的預設屬性掛載到defaultProps變數下。這段程式碼之前已經分析過了,參考 React原始碼分析2 — 元件和物件的建立(createClass,createElement)

.

這裡要提的一點是,初始化元件類只執行一次。可以把它簡單類比為Java中的Class物件。初始化元件類就是ClassLoader載入Class物件的過程。類物件的初始化不要錯誤理解成了例項物件的初始化。一個React元件類可能會在JSX中被多次呼叫,產生多個元件物件,但它只有一個類物件,也就是類載入後getDefaultProps就不會再呼叫了。

getInitialState

這個方法在建立元件例項物件的時候被呼叫,具體程式碼位於React.creatClass()的Constructor函式中。之前文章中已經分析了,參考 React原始碼分析2 — 元件和物件的建立(createClass,createElement)

每次建立React例項物件時,它都會被呼叫。

mountComponent

componentWillMount,render,componentDidMount都是在mountComponent中被呼叫。在React原始碼分析3 — React元件插入DOM流程一文中,我們講過mountComponent被呼叫的時機。它是在渲染新的ReactComponent中被呼叫的。輸入ReactComponent,返回元件對應的HTML。把這個HTML插入到DOM中,就可以生成元件對應的DOM物件了。所以mountComponent尤其關鍵。

和Java中的多型一樣,不同的React元件的mountComponent實現都有所區別。下面我們來重點分析React自定義元件類,也就是ReactCompositeComponent的mountComponent。

  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
    this._context = context;
    this._mountOrder = nextMountID++;
    this._nativeParent = nativeParent;
    this._nativeContainerInfo = nativeContainerInfo;

    // 做propTypes是否合法的判斷,這個只在開發階段有用
    var publicProps = this._processProps(this._currentElement.props);
    var publicContext = this._processContext(context);

    var Component = this._currentElement.type;

    // 初始化公共類
    var inst = this._constructComponent(publicProps, publicContext);
    var renderedElement;

    // inst或者inst.render為空對應的是stateless元件,也就是無狀態元件
    // 無狀態元件沒有例項物件,它本質上只是一個返回JSX的函式而已。是一種輕量級的React元件
    if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
      renderedElement = inst;
      warnIfInvalidElement(Component, renderedElement);
      inst = new StatelessComponent(Component);
    }

    // 設定變數
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = ReactUpdateQueue;
    this._instance = inst;

    // 儲存例項物件的引用到map中,方便以後查詢
    ReactInstanceMap.set(inst, this);

    // 初始化state,佇列等
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    var markup;
    if (inst.unstable_handleError) {
      // 掛載時出錯,進行一些錯誤處理,然後performInitialMount,初始化掛載
      markup = this.performInitialMountWithErrorHandling(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } else {
      // 初始化掛載
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }

    if (inst.componentDidMount) {
      // 呼叫componentDidMount,以事務的形式
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }

    return markup;
  },

mountComponent先做例項物件的props,state等初始化,然後呼叫performInitialMount初始化掛載,完成後呼叫componentDidMount。這個呼叫鏈還是很清晰的。下面我們重點來分析performInitialMountWithErrorHandling和performInitialMount

  performInitialMountWithErrorHandling: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var markup;
    var checkpoint = transaction.checkpoint();
    try {
      // 放到try-catch中,如果沒有出錯則呼叫performInitialMount初始化掛載。可見這裡沒有什麼特別的操作,也就是做一些錯誤處理而已
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } catch (e) {
      // handleError,解除安裝元件,然後重新performInitialMount初始化掛載
      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();

      this._renderedComponent.unmountComponent(true);
      transaction.rollback(checkpoint);

      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }
    return markup;
  },

可見performInitialMountWithErrorHandling只是多了一層錯誤處理而已,關鍵還是在performInitialMount中。

  performInitialMount: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var inst = this._instance;
    if (inst.componentWillMount) {
      // render前呼叫componentWillMount
      inst.componentWillMount();
      // 將state提前合併,故在componentWillMount中呼叫setState不會觸發重新render,而是做一次state合併。這樣做的目的是減少不必要的重新渲染
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }

    // 如果不是stateless,即無狀態元件,則呼叫render,返回ReactElement
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    // 得到元件型別,如空元件ReactNodeTypes.EMPTY,自定義React元件ReactNodeTypes.COMPOSITE,DOM原生元件ReactNodeTypes.NATIVE
    this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
    // 由ReactElement生成ReactComponent,這個方法在之前講解過。根據不同type建立不同Component物件
    // 參考 http://blog.csdn.net/u013510838/article/details/55669769
    this._renderedComponent = this._instantiateReactComponent(renderedElement);

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

    return markup;
  },

performInitialMount中先呼叫componentWillMount(),再將setState()產生的state改變進行state合併,然後呼叫_renderValidatedComponent()返回ReactElement,它會呼叫render()方法。然後由ReactElement建立ReactComponent。最後進行遞迴渲染。下面來看renderValidatedComponent()

  _renderValidatedComponent: function () {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }
    !(
    return renderedComponent;
  },

  _renderValidatedComponentWithoutOwnerOrContext: function () {
    var inst = this._instance;
    // 呼叫render方法,得到ReactElement。JSX經過babel轉譯後其實就是createElement()方法。這一點在前面也講解過
    var renderedComponent = inst.render();
    return renderedComponent;
  },

3 存在期生命週期

元件例項物件已經生成時,我們可以通過setState()來更新元件。setState機制後面會有單獨文章分析,現在只用知道它會呼叫updateComponent()來完成更新即可。下面來分析updateComponent

updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext
  ) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    var nextProps;

    // context物件如果有改動,則檢查propTypes等,這在開發階段可以報錯提醒
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    // 如果父元素型別相同,則跳過propTypes型別檢查
    if (prevParentElement === nextParentElement) {
      nextProps = nextParentElement.props;
    } else {
      nextProps = this._processProps(nextParentElement.props);
      willReceive = true;
    }

    // 呼叫componentWillReceiveProps,如果通過setState進入的updateComponent,則沒有這一步
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }

    // 提前合併state,componentWillReceiveProps中呼叫setState不會重新渲染,在此處做合併即可,因為後面也是要呼叫render的
    // 這樣可以避免沒必要的渲染
    var nextState = this._processPendingState(nextProps, nextContext);

    // 呼叫shouldComponentUpdate給shouldUpdate賦值
    // 如果通過forceUpdate進入的updateComponent,即_pendingForceUpdate不為空,則不用判斷shouldComponentUpdate.
    var shouldUpdate = true;
    if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
      shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
    }

    // 如果shouldUpdate為true,則會執行渲染,否則不會
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // 執行更新渲染,後面詳細分析
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      // shouldUpdate為false,則不會更新渲染
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
},

updateComponent中,先呼叫componentWillReceiveProps,然後合併setState導致的state變化。然後呼叫shouldComponentUpdate判斷是否需要更新渲染。如果需要,則呼叫_performComponentUpdate執行渲染更新,下面接著分析performComponentUpdate。

_performComponentUpdate: function(nextElement,nextProps,nextState,nextContext,transaction,
                                     unmaskedContext
  ) {
    var inst = this._instance;

    // 判斷是否已經update了
    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    // render前呼叫componentWillUpdate
    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    // state props等屬性設定到內部變數inst上
    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    // 內部會呼叫render方法,重新解析ReactElement並得到HTML
    this._updateRenderedComponent(transaction, unmaskedContext);

    // render後呼叫componentDidUpdate
    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
},

_performComponentUpdate會呼叫componentWillUpdate,然後在呼叫updateRenderedComponent進行更新渲染,最後呼叫componentDidUpdate。下面來看看updateRenderedComponent中怎麼呼叫render方法的。

_updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;

    // _renderValidatedComponent內部會呼叫render,得到ReactElement
    var nextRenderedElement = this._renderValidatedComponent();

    // 判斷是否做DOM diff。React為了簡化遞迴diff,認為元件層級不變,且type和key不變(key用於listView等元件,很多時候我們沒有設定type)才update,否則先unmount再重新mount
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      // 遞迴updateComponent,更新子元件的Virtual DOM
      ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
    } else {
      var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);

      // 不做DOM diff,則先解除安裝掉,然後再載入。也就是先unMountComponent,再mountComponent
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);

      // 由ReactElement建立ReactComponent
      this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);

      // mountComponent掛載元件,得到元件對應HTML
      var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, transaction, this._nativeParent, this._nativeContainerInfo, this._processChildContext(context));

      // 將HTML插入DOM中
      this._replaceNodeWithMarkup(oldNativeNode, nextMarkup, prevComponentInstance);
    }
},

_renderValidatedComponent: function() {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent =
        this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }

    return renderedComponent;
},

_renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    // 看到render方法了把,應該放心了把~
    var renderedComponent = inst.render();

    return renderedComponent;
},

和mountComponent中一樣,updateComponent也是用遞迴的方式將各子元件進行update的。這裡要特別注意的是DOM diff。DOM diff是React中渲染加速的關鍵所在,它會幫我們算出virtual DOM中真正變化的部分,並對這部分進行原生DOM操作。為了避免迴圈遞迴對比節點的低效率,React中做了假設,即只對層級不變,type不變,key不變的元件進行Virtual DOM更新。這其中的關鍵是shouldUpdateReactComponent,下面分析

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  // React DOM diff演算法
  // 如果前後兩次為數字或者字元,則認為只需要update(處理文字元素)
  // 如果前後兩次為DOM元素或React元素,則必須在同一層級內,且type和key不變(key用於listView等元件,很多時候我們沒有設定type)才update,否則先unmount再重新mount
  if (prevType === 'string' || prevType === 'number') {
    return (nextType === 'string' || nextType === 'number');
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

4 銷燬

前面提到過,更新元件時,如果不滿足DOM diff條件,會先unmountComponent, 然後再mountComponent,下面我們來分析下unmountComponent時都發生了什麼事。和mountComponent的多型一樣,不同type的ReactComponent也會有不同的unmountComponent行為。我們來分析下React自定義元件,也就是ReactCompositeComponent中的unmountComponent。

  unmountComponent: function(safely) {
    if (!this._renderedComponent) {
      return;
    }
    var inst = this._instance;

    // 呼叫componentWillUnmount
    if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) {
      inst._calledComponentWillUnmount = true;
      // 安全模式下,將componentWillUnmount包在try-catch中。否則直接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;
    }

    // reset等待佇列和其他等待狀態
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    this._pendingCallbacks = null;
    this._pendingElement = null;

    // reset內部變數,防止記憶體洩漏
    this._context = null;
    this._rootNodeID = null;
    this._topLevelWrapper = null;

    // 將元件從map中移除,還記得我們在mountComponent中將它加入了map中的吧
    ReactInstanceMap.remove(inst);
  },

可見,unmountComponent還是比較簡單的,它就做三件事

  1. 呼叫componentWillUnmount()
  2. 遞迴呼叫unmountComponent(),銷燬子元件
  3. 將內部變數置空,防止記憶體洩漏

5 總結

React自定義元件建立期,存在期,銷燬期三個階段的生命週期呼叫上面都講完了。三個入口函式mountComponent,updateComponent,unmountComponent尤其關鍵。大家如果有興趣,還可以自行分析ReactDOMEmptyComponent,ReactDOMComponent和ReactDOMTextComponent的這三個方法。

深入學習React生命週期原始碼可以幫我們理清各個方法的呼叫順序,明白它們都是什麼時候被呼叫的,哪些條件下才會被呼叫等等。閱讀原始碼雖然有點枯燥,但能夠大大加深對上層API介面的理解,並體會設計者設計這些API的良苦用心。