React 生命週期淺談

請叫我王磊同學發表於2017-05-18

React生命週期淺談

  React學習過程中,對於元件最重要的(也可能是之一)的莫過於是元件的生命週期了,React相當於使用狀態來對映到介面輸出,通過狀態的改變從而改變介面效果。在狀態的改變過程中,必須要經歷元件的生命週期。

  React會經歷三個階段:mount、update、unmount,每個階段對應兩個生命週期(ummount除外):Will(對應進入)與Did(對應結束),因而存在五個對應的方法,並且在update階段存在兩種特殊的方法:shouldComponentUpdatecomponentWillReceiveProps,這
些函式基本構成了React的生命週期。如下圖所示:

React 生命週期淺談
life-cycle

  上圖中的getDefaultPropsgetInitialState分別對應ES6中的static defaultProps = {}與建構函式construct中的this.state ={}賦值。下面我們按照上圖的過程依次介紹:(介紹主要以React.createClass為例,基本等同於extends React.Component)

React生命週期

初次渲染

//本文程式碼基於15.0,只刪選其中有用的部分,註釋來源於《深入React技術棧》
var React = {
  //...
  createClass: ReactClass.createClass
  //...
};

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;

      // ReactClass 沒有建構函式,通過 getInitialState 和 componentWillMount 來代替
      var initialState = this.getInitialState ? this.getInitialState() : null;
      this.state = initialState;
    };

    // 原型繼承父類
    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];

    // 合併 mixin
    injectedMixins.forEach(
      mixSpecIntoComponent.bind(null, Constructor)
    );

    mixSpecIntoComponent(Constructor, spec);

    // 所有 mixin 合併後初始化 defaultProps(在整個生命週期中,getDefaultProps 只執行一次)
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }
    // 減少查詢並設定原型的時間
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }
    return Constructor;
  },
};複製程式碼

  總結一下上面的程式碼React.createClass返回函式Constructor(props, context, updater)用來生成元件的例項,而React.createClass執行的時候會執行包括:合併mixin,獲取預設屬性defaultProps將其賦值到Constructor的原型中,並且也將傳入React.createClass中的方法賦值到Constructor的原型中,以縮短再次查詢方法的時間。
  在這個函式中,我們關心的部分其實主要集中在:

if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
}複製程式碼

  我們發現在呼叫React.createClass,已經執行了getDefaultProps(),並將其賦值於Constructor的原型中,所以我們對照宣告週期圖可以得到:

React中的getDefaultProps()僅會在整個生命週期中只執行一次,並且初始化的例項都會共享該defaultProps

  ReactCompositeComponent中的mountComponentupdateComponentunmountComponent分別對應於React中mount、update、unmount階段的處理,首先大致看一下mount階段的簡要程式碼:

// 當元件掛載時,會分配一個遞增編號,表示執行 ReactUpdates 時更新元件的順序
var nextMountID = 1;

var ReactCompositeComponent = {
    /**
     * 元件初始化,渲染、註冊事件
     * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
     * @param {?object} hostParent
     * @param {?object} hostContainerInfo
     * @param {?object} context
     * @return {?string} 返回的markup會被插入DOM中.
     * @final
     * @internal
     */
    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 = this._constructComponent(publicProps, publicContext);
        var renderedElement;

        // 用於判斷元件是否為 stateless,無狀態元件沒有狀態更新佇列,它只專注於渲染
        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;

        // 將例項儲存為一個引用
        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) {
            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) {
            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
        );
        // render 遞迴渲染
        var markup = ReactReconciler.mountComponent(this._renderedComponent, transaction, nativeParent,
            nativeContainerInfo, this._processChildContext(context));

        return markup;
    }
}複製程式碼

  我們現在只關心生命週期相關的程式碼,初始化及其他的程式碼暫時不考慮,我們發現初始化state之後會進入渲染的步驟,根據是否存在錯誤,選擇性執行performInitialMountWithErrorHandlingperformInitialMount,我們僅考慮正常情況下的performInitialMount

// 如果存在 componentWillMount,則呼叫
 if (inst.componentWillMount) {
     inst.componentWillMount();
     // componentWillMount 呼叫 setState 時,不會觸發 re-render 而是自動提前合併
     if (this._pendingStateQueue) {
         inst.state = this._processPendingState(inst.props, inst.context);
     }
 }複製程式碼

  如果存在componentWillMount則執行,如果在componentWillMount執行了setState方法,在componentWillMount並不會得到已經更新的state,因為我們發現的state的合併過程是在componentWillMount結束後才執行的。然後在performInitialMount(為例)會進行遞迴渲染,
然後在遞迴執行結束後,返回markup(返回的markup會被插入DOM中)。然後,如果存在 componentDidMount。並且由於渲染的過程都是遞迴的,我們可以綜合得到渲染階段的生命週期(包括子節點)如下:

React 生命週期淺談

更新階段

  首先還是看一下簡要的更新階段的程式碼:

var ReactCompositeComponent = {
    /**
     * 更新已經渲染的元件。componentWillReceiveProps和shouldComponentUpdate方法將會被呼叫
     * 然後,(更新的過程沒有被省略),其餘的更新階段的生命週期都會被呼叫,對應的DOM會被更新。
     * @param {ReactReconcileTransaction} transaction
     * @param {ReactElement} prevParentElement
     * @param {ReactElement} nextParentElement
     * @internal
     * @overridable
     */

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

        // Determine if the context has changed or not
        if (this._context === nextUnmaskedContext) {
            nextContext = inst.context;
        } else {
            nextContext = this._processContext(nextUnmaskedContext);
            willReceive = true;
        }

        var prevProps = prevParentElement.props;
        var nextProps = nextParentElement.props;

        // Not a simple state update but a props update
        if (prevParentElement !== nextParentElement) {
            willReceive = true;
        }

        // 如果存在 componentWillReceiveProps,則呼叫
        if (willReceive && inst.componentWillReceiveProps) {
            inst.componentWillReceiveProps(nextProps, nextContext);
        }

        // 將新的 state 合併到更新佇列中,此時 nextState 為最新的 state
        var nextState = this._processPendingState(nextProps, nextContext);

        // 根據更新佇列和 shouldComponentUpdate 的狀態來判斷是否需要更新元件
        var shouldUpdate =
            this._pendingForceUpdate || !inst.shouldComponentUpdate ||
            inst.shouldComponentUpdate(nextProps, nextState, nextContext);

        if (shouldUpdate) {
            // 重置更新佇列
            this._pendingForceUpdate = false;
            // 即將更新 this.props、this.state 和 this.context
            this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction,
                nextUnmaskedContext);
        } else {
            // 如果確定元件不更新,仍然要設定 props 和 state
            this._currentElement = nextParentElement;
            this._context = nextUnmaskedContext;
            inst.props = nextProps;
            inst.state = nextState;
            inst.context = nextContext;
        }
    },

    //當確定元件需要更新時,則呼叫
    _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) {
        var inst = this._instance;
        var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
        var prevProps;
        var prevState;
        var prevContext;

        // 如果存在 componentDidUpdate,則將當前的 props、state 和 context 儲存一份
        if (hasComponentDidUpdate) {
            prevProps = inst.props;
            prevState = inst.state;
            prevContext = inst.context;
        }

        // 如果存在 componentWillUpdate,則呼叫
        if (inst.componentWillUpdate) {
            inst.componentWillUpdate(nextProps, nextState, nextContext);
        }

        this._currentElement = nextElement;
        this._context = unmaskedContext;

        // 更新 this.props、this.state 和 this.context
        inst.props = nextProps;
        inst.state = nextState;
        inst.context = nextContext;

        // 實現程式碼省略,遞迴呼叫 render 渲染元件
        this._updateRenderedComponent(transaction, unmaskedContext);

        // 當元件完成更新後,如果存在 componentDidUpdate,則呼叫
        if (hasComponentDidUpdate) {
            transaction.getReactMountReady().enqueue(
                inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
                inst
            );
        }
    }
}複製程式碼

  判斷更新階段是否需要呼叫componentWillReceiveProps主要通過如下,同樣我們只關心生命週期相關的程式碼,其他的程式碼暫時不考慮:

if (this._context === nextUnmaskedContext) {
    nextContext = inst.context;
} else {
    nextContext = this._processContext(nextUnmaskedContext);
    willReceive = true;
}

var prevProps = prevParentElement.props;
var nextProps = nextParentElement.props;

// Not a simple state update but a props update
if (prevParentElement !== nextParentElement) {
    willReceive = true;
}

// 如果存在 componentWillReceiveProps,則呼叫
if (willReceive && inst.componentWillReceiveProps) {
    inst.componentWillReceiveProps(nextProps, nextContext);
}複製程式碼

  所以我們可以分析得出只有在context發生變化或者parentElement前後不一致(prevParentElement !== nextParentElement)時,willReceive才為true,這時,如果存在componentWillReceiveProps,就會被呼叫。那麼我們需要了解的是parentElement儲存的是什麼資訊,parentElement儲存的資訊如下:

{
    $$typeof:Symbol(react.element),
    key:null,
    props:Object,
    ref:null,
    type: function Example(props),
    _owner:ReactCompositeComponentWrapper,
    _store:Object,
    _self:App,
    _source:Object,
    __proto__:Object
}複製程式碼

  我們發現,parentElement是不含父元件的state資訊的。因此我們還可以得到下面的結論: 如果父元件的props等資訊發生改變時,即使這個改變的屬性沒有傳入到子元件,但也會引起子元件的componentWillReceiveProps的執行
並且我們可以發現,如果在componentWillReceiveProps中呼叫setState,state是不會立即得到更新。state會在componentWillReceiveProps後合併,所以componentWillReceiveProps中是不能拿到新的state

需要注意的是

不能在 shouldComponentUpdatecomponentWillUpdate 中呼叫 setState,原因是shouldComponentUpdatecomponentWillUpdate呼叫setState會導致再次呼叫updateComponent,這會造成迴圈呼叫,直至耗光瀏覽器記憶體後崩潰。

var shouldUpdate =
    this._pendingForceUpdate || !inst.shouldComponentUpdate ||
    inst.shouldComponentUpdate(nextProps, nextState, nextContext);

if (shouldUpdate) {
    // 重置更新佇列
    this._pendingForceUpdate = false;
    // 即將更新 this.props、this.state 和 this.context
    this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction,
        nextUnmaskedContext);
} else {
    // 如果確定元件不更新,仍然要設定 props 和 state
    this._currentElement = nextParentElement;
    this._context = nextUnmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;
}複製程式碼

  然後我們會根據shouldComponentUpdate返回的內容,決定是否執行全部的宣告週期更新操作。如果返回false,就不會執行接下來的更新操作。但是,從上面看得出,即使shouldComponentUpdate返回了false,元件中的propsstate以及state的都會被更新(當然,呼叫了forceUpdate函式的話,會跳過shouldComponentUpdate的判斷過程。)
如果shouldComponentUpdate返回true或者沒有定義shouldComponentUpdate函式,就會進行進行元件更新。如果存在componentDidUpdate,會將更新前的statepropscontext保留一份備份。如果存在componentWillUpdate,則呼叫。接著遞迴呼叫render進行渲染更新。當元件完成更新後,如果存在componentDidUpdate函式就會被呼叫,
並將更新前的狀態備份和當前的狀態作為引數傳遞。

React 生命週期淺談

解除安裝階段

var ReactCompositeComponent = {
    /**
     * 釋放由`mountComponent`分配的資源.
     *
     * @final
     * @internal
     */
    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);
    }
}複製程式碼

  解除安裝階段非常簡單,如果存在componentWillUnmount函式,則會在更新前呼叫。然後遞迴呼叫清理渲染。最後將相關引數、更新佇列以及更新狀態進行重置為空。
  本來想接著寫一下setStateReact Transaction,發現自己太弱雞了,並沒有完全看懂,現在正在學習研究中,大家以後可以關注一下~

相關文章