React生命週期淺談
React學習過程中,對於元件最重要的(也可能是之一)的莫過於是元件的生命週期了,React相當於使用狀態來對映到介面輸出,通過狀態的改變從而改變介面效果。在狀態的改變過程中,必須要經歷元件的生命週期。
React會經歷三個階段:mount、update、unmount,每個階段對應兩個生命週期(ummount除外):Will(對應進入)與Did(對應結束),因而存在五個對應的方法,並且在update階段存在兩種特殊的方法:shouldComponentUpdate
與componentWillReceiveProps
,這
些函式基本構成了React的生命週期。如下圖所示:
上圖中的getDefaultProps
和getInitialState
分別對應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
中的mountComponent
、updateComponent
、unmountComponent
分別對應於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
之後會進入渲染的步驟,根據是否存在錯誤,選擇性執行performInitialMountWithErrorHandling
與performInitialMount
,我們僅考慮正常情況下的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。並且由於渲染的過程都是遞迴的,我們可以綜合得到渲染階段的生命週期(包括子節點)如下:
更新階段
首先還是看一下簡要的更新階段的程式碼:
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
。
需要注意的是
不能在
shouldComponentUpdate
和componentWillUpdate
中呼叫setState
,原因是shouldComponentUpdate
與componentWillUpdate
呼叫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
,元件中的props
和state
以及state
的都會被更新(當然,呼叫了forceUpdate
函式的話,會跳過shouldComponentUpdate
的判斷過程。)
如果shouldComponentUpdate
返回true
或者沒有定義shouldComponentUpdate
函式,就會進行進行元件更新。如果存在componentDidUpdate
,會將更新前的state
、props
和context
保留一份備份。如果存在componentWillUpdate
,則呼叫。接著遞迴呼叫render
進行渲染更新。當元件完成更新後,如果存在componentDidUpdate
函式就會被呼叫,
並將更新前的狀態備份和當前的狀態作為引數傳遞。
解除安裝階段
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
函式,則會在更新前呼叫。然後遞迴呼叫清理渲染。最後將相關引數、更新佇列以及更新狀態進行重置為空。
本來想接著寫一下setState
和React Transaction
,發現自己太弱雞了,並沒有完全看懂,現在正在學習研究中,大家以後可以關注一下~