React源碼筆記-mount過程

React源碼筆記鏈接

React源碼筆記-虛擬dom

React源碼筆記-mount過程

本文是React源碼筆記的第二篇,主要講解mount過程,也就是在調用ReactDOM.render後的一系列過程。由於mount過程是在虛擬dom的基礎上進行的,並且在mount過程中會用到ReactDOMTextComponent、ReactDOMComponent和ReactCompositeComponent三個類,這些內容在第一篇React源碼筆記-虛擬dom中都有一定的解析,所以對於沒看過那篇文章的同學強烈建議去看一下。

mountComponent

簡單的說react的mount過程就是把虛擬dom轉化為真正的dom,然後掛載到dom樹中。所以我們需要知道虛擬dom是如何轉化成真正的dom的,dom的轉化主要依賴於ReactDOMTextComponent、ReactDOMComponent和ReactCompositeComponent三個類的mountComponent方法。當然在調用mountComponent方法之前,需要通過instantiateReactComponent創建虛擬dom對應的ReactXXXComponent對象,並通過construct進行初始化。

Text Node虛擬dom轉化為dom

首先通過instantiateReactComponent創建ReactDOMTextComponent對象,然後調用construct進行初始化,下面是construct的核心代碼:

construct: function(text) {n this._currentElement = text;n this._stringText = + text;nn this._rootNodeID = null;n this._mountIndex = 0;n}n

代碼很簡單,就是把傳過來的text node的虛擬dom(string或者number)保存在對應的屬性中,然後通過mountComponent方法把虛擬dom轉化成真實的dom,下面是mountComponent的核心源碼:

mountComponent: function(rootID, transaction, context) {n ...n this._rootNodeID = rootID;n if (transaction.useCreateElement) {n var ownerDocument = context[ReactMount.ownerDocumentContextKey];n var el = ownerDocument.createElement(span);n DOMPropertyOperations.setAttributeForID(el, rootID);n // Populate node cachen ReactMount.getID(el)n setTextContent(el, this._stringText);n return el;n } else {n var escapedText = escapeTextContentForBrowser(this._stringText);n ...n return (n <span + DOMPropertyOperations.createMarkupForID(rootID) + > +n escapedText +n </span>n );n }n },n

可以看到mountComponent方法傳過來三個參數:

  1. rootID,dom的唯一標識,在diff演算法中會用到。
  2. transaction,事務對象,它對於mount過程和調用setState後的同步過程有著重要的作用,對於用法在React 源碼剖析系列 - 解密 setState一文中有比較詳細的解釋,對於感興趣的同學可以去閱讀一下,後面文章中會解釋transcation的作用。

  3. context,這個參數對於mount過程和調用setState同步過程的理解作用不大,所以這裡就不進行解釋了。

通過源碼可以看出mountComponent創建了包裹著_stringText的span並返回,這裡之所以會用span包裹是因為text node和element類型的node特性差距比較大,為了一致的處理text node和element類型的node,所以通過一個span來包裹(源碼中的ownerDocument是document)。

Element類型的虛擬dom轉化為dom

首先通過instantiateReactComponent創建ReactDOMComponent對象,然後調用construct進行初始化,下面是construct的核心代碼:

construct: function(element) {n this._currentElement = element;n}n

簡單的將虛擬dom賦給_currentElement屬性,然後通過mountComponent方法把虛擬dom轉化成真實的dom,下面是mountComponent的核心源碼:

mountComponent: function(rootID, transaction, context) {n this._rootNodeID = rootID;nn var props = this._currentElement.props;n n ...nn var mountImage;n if (transaction.useCreateElement) {n var ownerDocument = context[ReactMount.ownerDocumentContextKey];n var el = ownerDocument.createElement(this._currentElement.type);n DOMPropertyOperations.setAttributeForID(el, this._rootNodeID);n // Populate node cachen ReactMount.getID(el);n this._updateDOMProperties({}, props, transaction, el);n this._createInitialChildren(transaction, props, context, el);n mountImage = el;n } else {n var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);n var tagContent = this._createContentMarkup(transaction, props, context);n if (!tagContent && omittedCloseTags[this._tag]) {n mountImage = tagOpen + />;n } else {n mountImage =n tagOpen + > + tagContent + </ + this._currentElement.type + >;n }n }nn ...nn return mountImage;n }n

因為代碼會根據transaction.useCreateElement來創建element類型的node或者markup(node的string版本),本質上一致的,所以這裡我們只分析創建element類型node的過程。首先通過虛擬dom的type創建對應的element node,然後調用_updateDOMProperties方法設置屬性,最後調用_createInitialChildren來創建子元素。

_updateDOMProperties的核心代碼如下:

_updateDOMProperties: function(lastProps, nextProps, transaction, node) {n var propKey;n var styleName;n var styleUpdates;n for (propKey in lastProps) {n if (nextProps.hasOwnProperty(propKey) ||n !lastProps.hasOwnProperty(propKey)) {n continue;n }n if (propKey === STYLE) {n var lastStyle = this._previousStyleCopy;n for (styleName in lastStyle) {n if (lastStyle.hasOwnProperty(styleName)) {n styleUpdates = styleUpdates || {};n styleUpdates[styleName] = ;n }n }n this._previousStyleCopy = null;n } else if (registrationNameModules.hasOwnProperty(propKey)) {n if (lastProps[propKey]) {n deleteListener(this._rootNodeID, propKey);n }n } else if (n DOMProperty.properties[propKey] ||n DOMProperty.isCustomAttribute(propKey)) {n if (!node) {n node = ReactMount.getNode(this._rootNodeID);n }n DOMPropertyOperations.deleteValueForProperty(node, propKey);n }n }n for (propKey in nextProps) {n var nextProp = nextProps[propKey];n var lastProp = propKey === STYLE ?n this._previousStyleCopy :n lastProps[propKey];n if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp) {n continue;n }n if (propKey === STYLE) {n if (nextProp) {n nextProp = this._previousStyleCopy = assign({}, nextProp);n } else {n this._previousStyleCopy = null;n }n if (lastProp) {n // Unset styles on `lastProp` but not on `nextProp`.n for (styleName in lastProp) {n if (lastProp.hasOwnProperty(styleName) &&n (!nextProp || !nextProp.hasOwnProperty(styleName))) {n styleUpdates = styleUpdates || {};n styleUpdates[styleName] = ;n }n }n // Update styles that changed since `lastProp`.n for (styleName in nextProp) {n if (nextProp.hasOwnProperty(styleName) &&n lastProp[styleName] !== nextProp[styleName]) {n styleUpdates = styleUpdates || {};n styleUpdates[styleName] = nextProp[styleName];n }n }n } else {n // Relies on `updateStylesByID` not mutating `styleUpdates`.n styleUpdates = nextProp;n }n } else if (registrationNameModules.hasOwnProperty(propKey)) {n if (nextProp) {n enqueuePutListener(this._rootNodeID, propKey, nextProp, transaction);n } else if (lastProp) {n deleteListener(this._rootNodeID, propKey);n }n } else if (isCustomComponent(this._tag, nextProps)) {n if (!node) {n node = ReactMount.getNode(this._rootNodeID);n }n if (propKey === CHILDREN) {n nextProp = null;n }n DOMPropertyOperations.setValueForAttribute(n node,n propKey,n nextPropn );n } else if (n DOMProperty.properties[propKey] ||n DOMProperty.isCustomAttribute(propKey)) {n if (!node) {n node = ReactMount.getNode(this._rootNodeID);n }n n if (nextProp != null) {n DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);n } else {n DOMPropertyOperations.deleteValueForProperty(node, propKey);n }n }n }n if (styleUpdates) {n if (!node) {n node = ReactMount.getNode(this._rootNodeID);n }n CSSPropertyOperations.setValueForStyles(node, styleUpdates);n }n },n

對於屬性的設置需要注意的地方有三個:

  1. 將之前有現在沒有的屬性提前刪除掉。
  2. 對於事件屬性是通過代理給document來實現的(這裡就不詳細的說明了)。
  3. 對於style屬性,由於傳過來的是一個對象,所以需要序列化成一個string。

_createInitialChildren的核心代碼如下:

var CONTENT_TYPES = {string: true, number: true};nn_createInitialChildren: function(transaction, props, context, el) {n // Intentional use of != to avoid catching zero/false.n var innerHTML = props.dangerouslySetInnerHTML;n if (innerHTML != null) {n if (innerHTML.__html != null) {n setInnerHTML(el, innerHTML.__html);n }n } else {n var contentToUse =n CONTENT_TYPES[typeof props.children] ? props.children : null;n var childrenToUse = contentToUse != null ? null : props.children;n if (contentToUse != null) {n // TODO: Validate that text is allowed as a child of this noden setTextContent(el, contentToUse);n } else if (childrenToUse != null) {n var mountImages = this.mountChildren(n childrenToUse,n transaction,n contextn );n for (var i = 0; i < mountImages.length; i++) {n el.appendChild(mountImages[i]);n }n }n }n },n

如果設置了dangerouslySetInnerHTML屬性或者props.children(子元素的虛擬dom)是一個text node類型的虛擬dom的話直接元素的innerHTML或textContent屬性,如果不是則調用mountChildren方法。ReactDOMComponent的mountChildren方法就是ReactMultiChild的mountChildren方法,下面我們來看看它的源碼:

mountChildren: function(nestedChildren, transaction, context) {n var children = this._reconcilerInstantiateChildren(n nestedChildren, transaction, contextn );n this._renderedChildren = children;n var mountImages = [];n var index = 0;n for (var name in children) {n if (children.hasOwnProperty(name)) {n var child = children[name];n var rootID = this._rootNodeID + name;n var mountImage = ReactReconciler.mountComponent(n child,n rootID,n transaction,n contextn );n child._mountIndex = index++;n mountImages.push(mountImage);n }n }n return mountImages;n }n

首先通過_reconcilerInstantiateChildren將nestedChildren虛擬dom轉化成對應的ReactXXXComponent對象,然後通過ReactReconciler.mountComponent創建各個虛擬dom對應的真實dom,ReactReconciler.mountComponent主要就是調用各個ReactXXXComponent對象的mountComponent方法,核心代碼如下:

mountComponent: function(internalInstance, rootID, transaction, context) {n var markup = internalInstance.mountComponent(rootID, transaction, context);nn ...nn return markup;n },n

繼承React.Component的虛擬dom轉化為dom

首先通過instantiateReactComponent創建ReactCompositeComponent對象,然後調用construct進行初始化,下面是construct的核心代碼:

construct: function(element) {n this._currentElement = element;n this._rootNodeID = null;n this._instance = null;nn // See ReactUpdateQueuen this._pendingElement = null;n this._pendingStateQueue = null;n this._pendingReplaceState = false;n this._pendingForceUpdate = false;nn this._renderedComponent = null;nn this._context = null;n this._mountOrder = 0;n this._topLevelWrapper = null;nn // See ReactUpdates and ReactUpdateQueue.n this._pendingCallbacks = null;n },n

初始化一些屬性,並把虛擬dom賦給_currentElement屬性,然後通過mountComponent方法把虛擬dom轉化成真實的dom,下面是mountComponent的核心源碼(為了方便理解,刪除了一些代碼,並做了少量修改):

mountComponent: function(rootID, transaction, context) {n this._context = context;n this._mountOrder = nextMountID++;n this._rootNodeID = rootID;nn var publicProps = this._processProps(this._currentElement.props);n var publicContext = this._processContext(context);nn var Component = this._currentElement.type;nn // Initialize the public classn var inst;n var renderedElement;nn ....nn inst = new Component(publicProps, publicContext, ReactUpdateQueue);nn ....nn inst.props = publicProps;n inst.context = publicContext;n inst.refs = emptyObject;n inst.updater = ReactUpdateQueue;nn this._instance = inst;n n ...nn var initialState = inst.state;n if (initialState === undefined) {n inst.state = initialState = null;n }n n this._pendingStateQueue = null;n this._pendingReplaceState = false;n this._pendingForceUpdate = false;nn if (inst.componentWillMount) {n inst.componentWillMount();nn if (this._pendingStateQueue) {n inst.state = this._processPendingState(inst.props, inst.context);n }n }nn if (renderedElement === undefined) {n renderedElement = this._renderValidatedComponent();n }nn this._renderedComponent = this._instantiateReactComponent(n renderedElementn );nn var markup = ReactReconciler.mountComponent(n this._renderedComponent,n rootID,n transaction,n this._processChildContext(context)n );n if (inst.componentDidMount) {n transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);n }nn return markup;n },n

首先通過type(繼承React.Component的構造函數)創建對應的對象,並對常用的props、context、state等屬性進行設置。然後判斷是否有componentWillMount生命周期函數,如果有的話就調用並判斷在componentWillMount方法中是否調用了setState等方法(setState等方法會把新的state保存在_pendingStateQueue隊列中),如果調用了的話就會對state進行合併或者替換處理。之後調用_renderValidatedComponent來間接的調用inst的render方法獲取對應的虛擬dom,調用_instantiateReactComponent創建對應的ReactXXXComponent,這裡可以解析為什麼React.Component的render方法為什麼只能返回單個元素。最後通過ReactReconciler.mountComponent間接的調用ReactXXXComponent的mountComponent方法產生對應的node節點或者markup並返回。

這裡有一個地方值得注意一下,就是最後判斷是否有componentDidMount生命周期函數,如果有的話,保存到transaction的一個隊列中。這裡是為了能夠在所有的虛擬dom完全轉化dom樹後調用componentDidMount函數,這裡也能夠很好的說明為什麼子元素的componentDidMount會比父元素的componentDidMount先調用,這是因為子元素的會在父元素之前先執行完componentDidMount方法。

mount過程

在知道了三種虛擬dom是如何轉化成dom之後,讓我們從ReactDOM.render方法開始說明react的mount過程吧。ReactDOM.render方法就是ReactMount.render方法,下面是它的源碼:

render: function(nextElement, container, callback) {n return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);n},n

簡單的調用了_renderSubtreeIntoContainer方法,繼續看這個方法的核心代碼:

_renderSubtreeIntoContainer: function(parentComponent, nextElement, container, callback) {n n ....nn var nextWrappedElement = new ReactElement(n TopLevelWrapper, // ReactElement的type屬性n null,n null,n null,n null,n null,n nextElement // ReactElement的props屬性n );n n ....nn var component = ReactMount._renderNewRootComponent(n nextWrappedElement,n container,n shouldReuseMarkup,n parentComponent != null ?n parentComponent._reactInternalInstance._processChildContext(n parentComponent._reactInternalInstance._contextn ) :n emptyObjectn )._renderedComponent.getPublicInstance();n n ....nn return component;n },nn var TopLevelWrapper = function() {};n TopLevelWrapper.prototype.isReactComponent = {};n n TopLevelWrapper.prototype.render = function() {n // this.props is actually a ReactElementn return this.props;n };n

首先通過ReactElement創建一個虛擬dom,其中TopLevelWrapper是ReactElement的type屬性,nextElement是ReactElement的props屬性,那麼TopLevelWrapper是什麼呢?其實很簡單有點像繼承了React.Component的子類,然後render方法返回的props屬性,也就是ReacDOM.render方法的第一個參數。創建虛擬dom後調用_renderNewRootComponent,核心代碼如下:

_renderNewRootComponent: function(n nextElement,n container,n shouldReuseMarkup,n contextn ) {n var componentInstance = instantiateReactComponent(nextElement, null);n n ....nn ReactUpdates.batchedUpdates(n batchedMountComponentIntoNode,n componentInstance,n reactRootID,n container,n shouldReuseMarkup,n contextn );n n ....nn return componentInstance;n },n

首先通過instantiateReactComponent方法創建虛擬dom對應的ReactXXXComponent對象,然後ReactUpdates.batchedUpdates批量的將虛擬dom轉化成dom,然後再掛載到ReactDOM.render第二個參數指定的dom上。讓我們來看看ReactUpdates.batchedUpdates的源碼:

function batchedUpdates(callback, a, b, c, d, e) {n ensureInjected();n batchingStrategy.batchedUpdates(callback, a, b, c, d, e);n}n

簡單的把參數代理給了batchingStrategy.batchedUpdates,這裡的batchingStrategy對應的是ReactDefaultBatchingStrategy,來看看ReactDefaultBatchingStrategy.batchedUpdates:

var ReactDefaultBatchingStrategy = {n isBatchingUpdates: false,n n batchedUpdates: function(callback, a, b, c, d, e) {n var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;nn ReactDefaultBatchingStrategy.isBatchingUpdates = true;nn // The code is written this way to avoid extra allocationsn if (alreadyBatchingUpdates) {n callback(a, b, c, d, e);n } else {n transaction.perform(callback, null, a, b, c, d, e);n } n },n};n

isBatchingUpdates是用來標誌當前是否處於批量更新狀態(mount過程和調用setState後的同步過程都屬於批量更新狀態),如果是直接調用第一個傳過來的callback,並把其他參數傳給這個callback,如果不是的話,將isBatchingUpdates設為true,然後通過transaction來執行callback的調用。對於transcation的用法請參考React 源碼剖析系列 - 解密 setState一文中對transcation的解釋。這裡的transcation是用來收集在mount過程中哪些React.Component調用了setState,對於調用了setState的React.Component被標記為dirty,放到ReactUpdates中的dirtyComponents數組裡,等待mount過程結束後,在對dirty的React.Component批量更新,這個過程將會在下一篇講解react在setState後的同步更新(diff演算法)的文章中會有詳細的解釋。

ok,看到這裡需要我們回頭看看ReactMount._renderNewRootComponent方法里調用ReactUpdates.batchedUpdates時傳入第一個參數是什麼,可以看到是batchedMountComponentIntoNode方法,核心代碼如下:

function batchedMountComponentIntoNode(n componentInstance,n rootID,n container,n shouldReuseMarkup,n contextn) {n var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(n /* forceHTML */ shouldReuseMarkupn );n transaction.perform(n mountComponentIntoNode,n null,n componentInstance,n rootID,n container,n transaction,n shouldReuseMarkup,n contextn );n ReactUpdates.ReactReconcileTransaction.release(transaction);n}n

這個方法首先獲取一個新的transcation,這個transcation的作用是在mount過程中收集React.Component的componentDidMount生命周期函數,以便在mount過程結束後觸發componentDidMount生命周期函數的觸發,對於怎麼收集componentDidMount的,在繼承React.Component的虛擬dom轉化為dom一節中有相應的解釋。之後通過transcation來調用mountComponentIntoNode方法,核心源碼如下(為了方便理解,刪除了一些代碼,並做了少量修改):

function mountComponentIntoNode(n componentInstance,n rootID,n container,n transaction,n shouldReuseMarkup,n contextn) {nn ....nn var markup = ReactReconciler.mountComponent(n componentInstance, rootID, transaction, contextn );nn ....nn ReactMount._mountImageIntoNode(n markup,n container,n shouldReuseMarkup,n transactionn );n}n

首先通過ReactReconciler.mountComponent方法,把傳過來的componentInstance轉化成真實的dom節點(markup),然後通過ReactMount._mountImageIntoNode將markup掛載到container節點上,ReactMount._mountImageIntoNode核心代碼如下:

_mountImageIntoNode: function(n markup,n container,n shouldReuseMarkup,n transactionn ) {n n ....nn if (transaction.useCreateElement) {n while (container.lastChild) {n container.removeChild(container.lastChild);n }n container.appendChild(markup);n } else {n setInnerHTML(container, markup);n }n }n

總結

比較粗糙的分析了一下react的mount過程,對於看到這裡的讀者真的是非常的感謝,如果您發現文中有什麼問題,希望能夠指正。

推薦閱讀:

【源碼眾讀】進度報告,這不是一個假活動
【Vlpp源碼閱讀】集合篇(一)
深度學習一行一行敲vae-npvc網路-tensorflow版(convert.py)
Python 中的對象概述

TAG:React | 前端框架 | 源码阅读 |