React原始碼分析4 — React生命週期詳解
1 React生命週期流程
呼叫流程可以參看上圖。分為例項化,存在期和銷燬三個不同階段。介紹生命週期流程的文章很多,相信大部分同學也有所瞭解,我們就不詳細分析了。很多同學肯定有疑問,這些方法在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還是比較簡單的,它就做三件事
- 呼叫componentWillUnmount()
- 遞迴呼叫unmountComponent(),銷燬子元件
- 將內部變數置空,防止記憶體洩漏
5 總結
React自定義元件建立期,存在期,銷燬期三個階段的生命週期呼叫上面都講完了。三個入口函式mountComponent,updateComponent,unmountComponent尤其關鍵。大家如果有興趣,還可以自行分析ReactDOMEmptyComponent,ReactDOMComponent和ReactDOMTextComponent的這三個方法。
深入學習React生命週期原始碼可以幫我們理清各個方法的呼叫順序,明白它們都是什麼時候被呼叫的,哪些條件下才會被呼叫等等。閱讀原始碼雖然有點枯燥,但能夠大大加深對上層API介面的理解,並體會設計者設計這些API的良苦用心。