React源碼分析3 — React生命周期詳解
1 React生命周期流程
調用流程可以參看上圖。分為實例化,存在期和銷毀三個不同階段。介紹生命周期流程的文章很多,相信大部分同學也有所了解,我們就不詳細分析了。很多同學肯定有疑問,這些方法在React內部是在哪些方法中被調用的呢,他們觸發的時機又是什麼時候呢。下面我們來詳細分析。
2 實例化生命周期
getDefaultProps
在React.creatClass()初始化組件類時,會調用getDefaultProps(),將返回的默認屬性掛載到defaultProps變數下。這段代碼之前已經分析過了,參考 React源碼分析1 — 組件和對象的創建(createClass,createElement).
這裡要提的一點是,初始化組件類只運行一次。可以把它簡單類比為Java中的Class對象。初始化組件類就是ClassLoader載入Class對象的過程。類對象的初始化不要錯誤理解成了實例對象的初始化。一個React組件類可能會在JSX中被多次調用,產生多個組件對象,但它只有一個類對象,也就是類載入後getDefaultProps就不會再調用了。
getInitialState
這個方法在創建組件實例對象的時候被調用,具體代碼位於React.creatClass()的Constructor函數中。之前文章中已經分析了,參考 React源碼分析1 — 組件和對象的創建(createClass,createElement)。
每次創建React實例對象時,它都會被調用。
mountComponent
componentWillMount,render,componentDidMount都是在mountComponent中被調用。在React源碼分析2 — 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的良苦用心。
推薦閱讀:
※關於閱讀開源項目的源碼,有哪些經驗值得分享?
※OpenStack虛擬機掛載數據卷過程分析
※國外有哪些優秀的源碼剖析類書籍?
※深度學習一行一行敲faster rcnn-keras版(2.3,網路測試)