1 React生命周期流程
調(diào)用流程可以參看上圖。分為實例化,存在期和銷毀三個不同階段。介紹生命周期流程的文章很多,相信大部分同學(xué)也有所了解,我們就不詳細分析了。很多同學(xué)肯定有疑問,這些方法在React內(nèi)部是在哪些方法中被調(diào)用的呢,他們觸發(fā)的時機又是什么時候呢。下面我們來詳細分析。
2 實例化生命周期
getDefaultProps
在React.creatClass()初始化組件類時,會調(diào)用getDefaultProps(),將返回的默認屬性掛載到defaultProps變量下。這段代碼之前已經(jīng)分析過了,參考
React源碼分析1 — 組件和對象的創(chuàng)建(createClass,createElement).
這里要提的一點是,初始化組件類只運行一次??梢园阉唵晤惐葹镴ava中的Class對象。初始化組件類就是ClassLoader加載Class對象的過程。類對象的初始化不要錯誤理解成了實例對象的初始化。一個React組件類可能會在JSX中被多次調(diào)用,產(chǎn)生多個組件對象,但它只有一個類對象,也就是類加載后getDefaultProps就不會再調(diào)用了。
getInitialState
這個方法在創(chuàng)建組件實例對象的時候被調(diào)用,具體代碼位于React.creatClass()的Constructor函數(shù)中。之前文章中已經(jīng)分析了,參考
React源碼分析1 — 組件和對象的創(chuàng)建(createClass,createElement)。
mountComponent
componentWillMount,render,componentDidMount都是在mountComponent中被調(diào)用。在
React源碼分析2 — React組件插入DOM流程
一文中,我們講過mountComponent被調(diào)用的時機。它是在渲染新的ReactComponent中被調(diào)用的。輸入ReactComponent,返回組件對應(yīng)的HTML。把這個HTML插入到DOM中,就可以生成組件對應(yīng)的DOM對象了。所以mountComponent尤其關(guān)鍵。不同的React組件的mountComponent實現(xiàn)都有所區(qū)別。下面我們來重點分析React自定義組件類,也就是ReactCompositeComponent的mountComponent。
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
this._context = context;
this._mountOrder = nextMountID++;
this._nativeParent = nativeParent;
this._nativeContainerInfo = nativeContainerInfo;
// 做propTypes是否合法的判斷,這個只在開發(fā)階段有用
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為空對應(yīng)的是stateless組件,也就是無狀態(tài)組件
// 無狀態(tài)組件沒有實例對象,它本質(zhì)上只是一個返回JSX的函數(shù)而已。是一種輕量級的React組件
if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
renderedElement = inst;
warnIfInvalidElement(Component, renderedElement);
inst = new StatelessComponent(Component);
}
// 設(shè)置變量
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) {
// 調(diào)用componentDidMount,以事務(wù)的形式
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
return markup;
},
mountComponent先做實例對象的props,state等初始化,然后調(diào)用performInitialMount初始化掛載,完成后調(diào)用componentDidMount。這個調(diào)用鏈還是很清晰的。下面我們重點來分析performInitialMountWithErrorHandling和performInitialMount
performInitialMountWithErrorHandling: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
var markup;
var checkpoint = transaction.checkpoint();
try {
// 放到try-catch中,如果沒有出錯則調(diào)用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只是多了一層錯誤處理而已,關(guān)鍵還是在performInitialMount中。
performInitialMount: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
var inst = this._instance;
if (inst.componentWillMount) {
// render前調(diào)用componentWillMount
inst.componentWillMount();
// 將state提前合并,故在componentWillMount中調(diào)用setState不會觸發(fā)重新render,而是做一次state合并。這樣做的目的是減少不必要的重新渲染
if (this._pendingStateQueue) {
inst.state = this._processPendingState(inst.props, inst.context);
}
}
// 如果不是stateless,即無狀態(tài)組件,則調(diào)用render,返回ReactElement
if (renderedElement === undefined) {
renderedElement = this._renderValidatedComponent();
}
// 得到組件類型,如空組件ReactNodeTypes.EMPTY,自定義React組件ReactNodeTypes.COMPOSITE,DOM原生組件ReactNodeTypes.NATIVE
this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
// 由ReactElement生成ReactComponent,這個方法在之前講解過。根據(jù)不同type創(chuàng)建不同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中先調(diào)用componentWillMount(),再將setState()產(chǎn)生的state改變進行state合并,然后調(diào)用_renderValidatedComponent()返回ReactElement,它會調(diào)用render()方法。然后由ReactElement創(chuàng)建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;
// 調(diào)用render方法,得到ReactElement。JSX經(jīng)過babel轉(zhuǎn)譯后其實就是createElement()方法。這一點在前面也講解過
var renderedComponent = inst.render();
return renderedComponent;
},
3 存在期生命周期
組件實例對象已經(jīng)生成時,我們可以通過setState()來更新組件。setState機制后面會有單獨文章分析,現(xiàn)在只用知道它會調(diào)用updateComponent()來完成更新即可。下面來分析updateComponent
updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext
) {
var inst = this._instance;
var willReceive = false;
var nextContext;
var nextProps;
// context對象如果有改動,則檢查propTypes等,這在開發(fā)階段可以報錯提醒
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;
}
// 調(diào)用componentWillReceiveProps,如果通過setState進入的updateComponent,則沒有這一步
if (willReceive && inst.componentWillReceiveProps) {
inst.componentWillReceiveProps(nextProps, nextContext);
}
// 提前合并state,componentWillReceiveProps中調(diào)用setState不會重新渲染,在此處做合并即可,因為后面也是要調(diào)用render的
// 這樣可以避免沒必要的渲染
var nextState = this._processPendingState(nextProps, nextContext);
// 調(diào)用shouldComponentUpdate給shouldUpdate賦值
// 如果通過forceUpdate進入的updateComponent,即_pendingForceUpdate不為空,則不用判斷shouldComponentUpdate.
var shouldUpdate = true;
if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
}
// 如果shouldUpdate為true,則會執(zhí)行渲染,否則不會
this._updateBatchNumber = null;
if (shouldUpdate) {
this._pendingForceUpdate = false;
// 執(zhí)行更新渲染,后面詳細分析
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中,先調(diào)用componentWillReceiveProps,然后合并setState導(dǎo)致的state變化。然后調(diào)用shouldComponentUpdate判斷是否需要更新渲染。如果需要,則調(diào)用_performComponentUpdate執(zhí)行渲染更新,下面接著分析performComponentUpdate。
_performComponentUpdate: function(nextElement,nextProps,nextState,nextContext,transaction,
unmaskedContext
) {
var inst = this._instance;
// 判斷是否已經(jīng)update了
var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
var prevProps;
var prevState;
var prevContext;
if (hasComponentDidUpdate) {
prevProps = inst.props;
prevState = inst.state;
prevContext = inst.context;
}
// render前調(diào)用componentWillUpdate
if (inst.componentWillUpdate) {
inst.componentWillUpdate(nextProps, nextState, nextContext);
}
// state props等屬性設(shè)置到內(nèi)部變量inst上
this._currentElement = nextElement;
this._context = unmaskedContext;
inst.props = nextProps;
inst.state = nextState;
inst.context = nextContext;
// 內(nèi)部會調(diào)用render方法,重新解析ReactElement并得到HTML
this._updateRenderedComponent(transaction, unmaskedContext);
// render后調(diào)用componentDidUpdate
if (hasComponentDidUpdate) {
transaction.getReactMountReady().enqueue(
inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
inst
);
}
},
_performComponentUpdate會調(diào)用componentWillUpdate,然后在調(diào)用updateRenderedComponent進行更新渲染,最后調(diào)用componentDidUpdate。下面來看看updateRenderedComponent中怎么調(diào)用render方法的。
_updateRenderedComponent: function(transaction, context) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
// _renderValidatedComponent內(nèi)部會調(diào)用render,得到ReactElement
var nextRenderedElement = this._renderValidatedComponent();
// 判斷是否做DOM diff。React為了簡化遞歸diff,認為組件層級不變,且type和key不變(key用于listView等組件,很多時候我們沒有設(shè)置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創(chuàng)建ReactComponent
this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);
// mountComponent掛載組件,得到組件對應(yīng)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方法了把,應(yīng)該放心了把~
var renderedComponent = inst.render();
return renderedComponent;
},
和mountComponent中一樣,updateComponent也是用遞歸的方式將各子組件進行update的。這里要特別注意的是DOM diff。DOM diff是React中渲染加速的關(guān)鍵所在,它會幫我們算出virtual DOM中真正變化的部分,并對這部分進行原生DOM操作。為了避免循環(huán)遞歸對比節(jié)點的低效率,React中做了假設(shè),即只對層級不變,type不變,key不變的組件進行Virtual DOM更新。這其中的關(guān)鍵是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算法
// 如果前后兩次為數(shù)字或者字符,則認為只需要update(處理文本元素)
// 如果前后兩次為DOM元素或React元素,則必須在同一層級內(nèi),且type和key不變(key用于listView等組件,很多時候我們沒有設(shè)置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時都發(fā)生了什么事。和mountComponent的多態(tài)一樣,不同type的ReactComponent也會有不同的unmountComponent行為。我們來分析下React自定義組件,也就是ReactCompositeComponent中的unmountComponent。
unmountComponent: function(safely) {
if (!this._renderedComponent) {
return;
}
var inst = this._instance;
// 調(diào)用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();
}
}
// 遞歸調(diào)用unMountComponent來銷毀子組件
if (this._renderedComponent) {
ReactReconciler.unmountComponent(this._renderedComponent, safely);
this._renderedNodeType = null;
this._renderedComponent = null;
this._instance = null;
}
// reset等待隊列和其他等待狀態(tài)
this._pendingStateQueue = null;
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
this._pendingCallbacks = null;
this._pendingElement = null;
// reset內(nèi)部變量,防止內(nèi)存泄漏
this._context = null;
this._rootNodeID = null;
this._topLevelWrapper = null;
// 將組件從map中移除,還記得我們在mountComponent中將它加入了map中的吧
ReactInstanceMap.remove(inst);
},
可見,unmountComponent還是比較簡單的,它就做三件事
調(diào)用componentWillUnmount()
遞歸調(diào)用unmountComponent(),銷毀子組件
將內(nèi)部變量置空,防止內(nèi)存泄漏
5 總結(jié)
React自定義組件創(chuàng)建期,存在期,銷毀期三個階段的生命周期調(diào)用上面都講完了。三個入口函數(shù)mountComponent,updateComponent,unmountComponent尤其關(guān)鍵。大家如果有興趣,還可以自行分析ReactDOMEmptyComponent,ReactDOMComponent和ReactDOMTextComponent的這三個方法。
深入學(xué)習(xí)React生命周期源碼可以幫我們理清各個方法的調(diào)用順序,明白它們都是什么時候被調(diào)用的,哪些條件下才會被調(diào)用等等。閱讀源碼雖然有點枯燥,但能夠大大加深對上層API接口的理解,并體會設(shè)計者設(shè)計這些API的良苦用心。