React原始碼解讀之componentMount

ZHONGJIAFENG7發表於2018-07-18

作為初級碼農不該天花亂墜大講情懷,一開始入坑了幾天對於很多地方充滿了愛迪生般的諸多疑問(莫名把自己誇了一波,XD),所以打算看一波原始碼,這個過程可以說是非常曲折,本人智商不高,看了四五遍部分原始碼後,一臉懵逼,於是在接下來的一週內處於渾渾噩噩,若即若離的抽離狀態,於是放棄瞭解讀,最近感覺學習react有兩個月了,公司大牛的教誨和啟發,打算原路折回再次拾起react這個胖小孩,不得不說有大牛的幫助真會讓你進步飛快。

這是本人的第一篇react相關的文章,本來只是留作自己筆記之用,結果筆記越寫越多,一方面是為了加深自己對於優雅的react的理解,另一方面為了給計劃學習react的旁友們提供一點微不足道的小思路。 當然一提到分析解讀原始碼,這幾個莊重的字眼的時候,首先是油然而生的濃濃的自豪感,自豪感不能白來,因此也是謹慎地翻牆看了很多別人的解讀,對於一些大神們解讀首先是敬佩,然後覺得應該仿效他們進行更多詳細的補充,當然寫的有所紕漏,不足之處還希望大神們指出。

廢話太多了,進入正題,下面是我自己列出的TODOList,在讀原始碼前應該需要理解一些相關的要點

1.什麼是JSX?

JSX 的官方定義是類 XML 語法的 ECMAScript 擴充套件。它完美地利用了 JavaScript 自帶的語法 和特性,並使用大家熟悉的 HTML 語法來建立虛擬元素。使用類 XML 語法的好處是標籤可以任意巢狀,我們可以像HTML一樣清晰地看到DOM樹

JSX 將 HTML 語法直接加入到 JavaScript程式碼中,在實際開發中,JSX在產品打包階段都已經編譯成純JavaScript,不會帶來任何副作用,反而會讓程式碼更加直觀並易於維護。

更多詳見:CSDN

2.React.createElement

React.createElement(type, config, children) 做了三件事:

  • 把 config裡的資料一項一項拷入props,
  • 拷貝 children 到 props.children,
  • 拷貝 type.defaultProps 到 props;

3.元件生命週期

React原始碼解讀之componentMount

4.renderedElement和ReactDOMComponent

ReactElement是React元素在記憶體中的表示形式,可以理解為一個資料類,包含type,key,refs,props等成員變數

ReactComponent是React元素的操作類,包含mountComponent(), updateComponent()等很多操作元件的方法,主要有ReactDOMComponent, ReactCompositeComponent, ReactDOMTextComponent, ReactDOMEmptyComponent四個型別

5.inst

inst是對於元件的例項化物件,主要包括props, refs, context, updater更新方法集, state等。

頂級元件的例項化屬性:

React原始碼解讀之componentMount
子元件的例項化屬性:
React原始碼解讀之componentMount

6._currentElement

currentElement即為每次例項化的renderedElement,為了在componentInstance儲存當前資訊 var ReactCompositeComponentWrapper = function (element) { this.construct(element); };

React原始碼解讀之componentMount

原始碼分析

接下來配合一個小例子來大概分析下react內部神祕的元件掛載操作
<!DOCTYPE html>
<html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>Document</title>
    	<script src="./js/react.js"></script>
    	<script src="./js/react-dom.js"></script>
    	<script src="./js/browser.js"></script>
    	<script type="text/babel">
    	  class Children extends React.Component {
    	  	constructor(...args) {
    	  		super(...args);
    	  	}
    
    	  	render() {
    	  		return <div>children</div>
    	  	}
    	  }
    
    		class Comp extends React.Component{
    			constructor(...args) {
    				super(...args);
    				this.state = {i: 0}
    			}
    			render(){
    				return <div onClick={() => {
    					this.setState({i: this.state.i + 1})
    				}}>Hello, world! {this.props.name}, 年齡{this.props.age} {this.state.i} <i>222</i><Children /></div>;
    			}
    		}
    		window.onload = function(){
    			var oDiv = document.getElementById('div1');
    			ReactDOM.render(
    				<Comp name="zjf" age='24'/>,
    				oDiv
    			);
    		}
    	</script>
	</head>
	<body>
		<div id="div1"><div>2222</div></div>
	</body>
</html>
複製程式碼

本次原始碼分析的版本號是v15.6.0(160之後變化很大有點看不懂),可以使用git reset --hard v15.6.0操作進行版本回退 首先函式的入口是reactDOM.render(), 這個函式可以放兩個引數,第一個為需要渲染的元件,第二個為第一個元件掛載的物件。 通過呼叫ReactDom.render() -> 呼叫ReactMount.render() -> 呼叫renderSubtreeIntoContainer, 在這個函式裡個人認為需要知道:

// parentComponent一般為null, nextElement,container分別為reactDOM.render中的前兩個引數
renderSubtreeIntoContainer(parentComponent, nextElement, container, callback){
	// ...
	// TopLevelWrapper為頂級容器,型別為object(其實是一個方法),內部有個rootID屬性,值得注意的是該方法原型鏈上有render方法,該方法是第一個被呼叫的,它應該很自豪
	var nextWrappedElement = React.createElement(TopLevelWrapper, {
	  child: nextElement
	});
	// 開始進入正軌,該方法內部會根據nextWrapperElement生成相應型別的元件
	var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance()
}
複製程式碼
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {

  // 例項化元件,通過nextElement.type判斷,string,object生成ReactDOMComponent, ReactCompositeComponent如果不存在nextElement則生成ReactEmptyComponent,如果typeof nextElement型別為string或者number直接生成ReactDOMTextComponent
  var componentInstance = instantiateReactComponent(nextElement, false);

  // The initial render is synchronous but any updates that happen during, rendering, in componentWillMount or componentDidMount, will be batched according to the current batching strategy.

  ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
  
  return componentInstance;
},
複製程式碼
// transaction.perform其實是事務,事務中簡單地說有initialize->執行perform第一個callback->close操作,準備在setState介紹
function batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context) {
  //
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
  /* useCreateElement */
  !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement);
  transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}
複製程式碼
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
  // 根據wrapperInstance來呼叫不同元件型別的mountComponent方法
  var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context, 0 /* parentDebugID */);
  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  // setInnerHTML(container, markup),最終會將markup虛擬節點插入真正的DOM樹
  ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
}
複製程式碼

mountComponent: 不同的React元件的mountComponent實現都有所區別,下面分析React自定義元件類

// 來到了元件的掛載,需要注意幾個變數:
renderedElement, _renderedComponent,
inst, ReactInstanceMap.set(inst, this), 
_pendingStateQueue, _pendingForceUpdate, _processPendingState,_processContext, 
componentWillMount, componentDidMount 
// 本質上是呼叫Component構造方法的新例項物件,這個instance上會新增,context,props,refs以及updater屬性(見圖二),後續使用Map的形式用此作為key,元件作為value,方便之後獲取元件,比如上面所說的type為TopLevelWrapper,構造其例項
var Component = this._currentElement.type;
var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
// inst或者inst.render為空對應的是stateless元件,也就是無狀態元件
// 無狀態元件沒有例項物件,它本質上只是一個返回JSX的函式而已。是一種輕量級的React元件
if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
  renderedElement = inst;
  warnIfInvalidElement(Component, renderedElement);
  inst = new StatelessComponent(Component);
}
// Store a reference from the instance back to the internal representation
ReactInstanceMap.set(inst, this);
this._pendingStateQueue = null;
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
// ...
// 初始化掛載
markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
// 將componentDidMount以事務的形式進行呼叫
transaction.getReactMountReady().enqueue(function () {
  measureLifeCyclePerf(function () {
    return inst.componentDidMount();
  }, _this._debugID, 'componentDidMount');
});
複製程式碼

圖二:

React原始碼解讀之componentMount

performInitialMount:

// render前呼叫componentWillMount
inst.componentWillMount()
// 將state提前合併,故在componentWillMount中呼叫setState不會觸發重新render,而是做一次state合併。這樣做的目的是減少不必要的重新渲染
// _processPendingState進行原有state的合併, _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); 以及設定this._pendingStateQueue = null,這也就意味著dirtyComponents進入下一次迴圈時,執行performUpdateIfNecessary不會再去更新元件
if (this._pendingStateQueue) {
    inst.state = this._processPendingState(inst.props, inst.context);
 }
 // 如果不是stateless,即無狀態元件,則呼叫render,返回ReactElement
if (renderedElement === undefined) {
    renderedElement = this._renderValidatedComponent();
}
var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
);
this._renderedComponent = child;

// 遞迴渲染,渲染子元件,返回markup,匹配同型別的元件,返回markup
var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID);
}
// 比如
	var markup = internalInstance.mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
複製程式碼

_renderValidatedComponent:

// 呼叫render方法,得到ReactElement。JSX經過babel轉譯後其實就是createElement()方法,比如上面所提到的TopLevelWrapper內有render方法(圖三,圖四)
var renderedComponent = inst.render();   
複製程式碼

圖三:

React原始碼解讀之componentMount

圖四:

React原始碼解讀之componentMount

由renderedElement.type型別可以知道所要生成的元件型別為reactDOMComponent,來看下這個物件下的mountComponent方法

if (namespaceURI === DOMNamespaces.html) {
   if (this._tag === 'script') {
     // 當插入標籤為script的時候react也進行了包裝,這樣script就只是innerHTML不會進行執行,不然會有注入的危險
     var div = ownerDocument.createElement('div');
     var type = this._currentElement.type;
     div.innerHTML = '<' + type + '></' + type + '>';
     el = div.removeChild(div.firstChild);
   } else if (props.is) {
     el = ownerDocument.createElement(this._currentElement.type, props.is);
   } else {
     // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
     // See discussion in https://github.com/facebook/react/pull/6896
     // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
     el = ownerDocument.createElement(this._currentElement.type);
   }
 } else {
   el = ownerDocument.createElementNS(namespaceURI, this._currentElement.type);
 }
 // Populate `_hostNode` on the rendered host/text component with the given DOM node.
 ReactDOMComponentTree.precacheNode(this, el);
 this._flags |= Flags.hasCachedChildNodes;
 if (!this._hostParent) {
	 // 在根節點上設定data-root屬性
	 DOMPropertyOperations.setAttributeForRoot(el);
 }
 this._updateDOMProperties(null, props, transaction);
 // 初始化lazyTree,返回例項    
 // node: node,
 // children: [],
 // html: null,
 // text: null,
 // toString: toString
 var lazyTree = DOMLazyTree(el);
 // 遍歷內部props,判斷props.children內部是string/number型別還是其他型別,如果是前者直接將內部children插入到node中去,否則就需要非string/number型別進行繼續渲染
 this._createInitialChildren(transaction, props, context, lazyTree);
 mountImage = lazyTree;
 return mountImage;
複製程式碼

上面程式碼其中有必要了解下DOMLazyTree的一些屬性方法因為之後會有呼叫以及_createInitialChildren,這個是將props.children轉換為innerHTML的關鍵

function DOMLazyTree(node) {
  return {
    node: node,
    children: [],
    html: null,
    text: null,
    toString: toString
  };
}

DOMLazyTree.insertTreeBefore = insertTreeBefore;
DOMLazyTree.replaceChildWithTree = replaceChildWithTree;
// 按序向節點的子節點列表的末尾新增新的子節點
DOMLazyTree.queueChild = queueChild;
// 按序插入HTML
DOMLazyTree.queueHTML = queueHTML;
// 按序插入文字
DOMLazyTree.queueText = queueText;
function queueChild(parentTree, childTree) {
  if (enableLazy) {
    parentTree.children.push(childTree);
  } else {
    parentTree.node.appendChild(childTree.node);
  }
}

function queueHTML(tree, html) {
  if (enableLazy) {
    tree.html = html;
  } else {
    setInnerHTML(tree.node, html);
  }
}

function queueText(tree, text) {
  if (enableLazy) {
    tree.text = text;
  } else {
    // 內部其實將node.textContent = text;
    setTextContent(tree.node, text);
  }
}
複製程式碼

看了這麼多是不是感覺到濃濃的基礎知識,insertBefore, appendChild, textContent, createElement,createElementNS,nodeType

  _createInitialChildren: function (transaction, props, context, lazyTree) {
    // Intentional use of != to avoid catching zero/false.
    var innerHTML = props.dangerouslySetInnerHTML;
    if (innerHTML != null) {
      if (innerHTML.__html != null) {
        DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
      }
    } else {
      // 這兩個是互斥的條件,contentToUse用來判讀是不是string,number,如果不是則返回null,childrenToUse生效
      var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
      var childrenToUse = contentToUse != null ? null : props.children;
      // TODO: Validate that text is allowed as a child of this node
      if (contentToUse != null) {
	    // 省略一些程式碼...
	    // 上面有說過將內部其實就是插入text的操作, node.concontentText = contentToUse
        DOMLazyTree.queueText(lazyTree, contentToUse);
      } else if (childrenToUse != null) {
        // 對於其他型別繼續進行渲染
        var mountImages = this.mountChildren(childrenToUse, transaction, context);
        for (var i = 0; i < mountImages.length; i++) {
          // 向節點的子節點列表的末尾新增新的子節點
          DOMLazyTree.queueChild(lazyTree, mountImages[i]);
        }
      }
    }
  },
複製程式碼
// 這兩個是互斥的條件,contentToUse用來判讀是不是string,number,如果不是則返回null,childrenToUse生效
var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
// 如果這個條件不為null
var childrenToUse = contentToUse != null ? null : props.children;
var mountImages = this.mountChildren(childrenToUse, transaction, context);
複製程式碼
mountChildren: function (nestedChildren, transaction, context) {
  // ...
  var mountImages = [];
  var index = 0;
  for (var name in children) {
    if (children.hasOwnProperty(name)) {
      var child = children[name];
      // 通過child的型別來例項化不同型別的元件
      var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._hostContainerInfo, context, selfDebugID);
      child._mountIndex = index++;
      mountImages.push(mountImage);
    }
  }
  return mountImages;
},
複製程式碼

總結

總的來說元件掛載大概可以概括為以下的步驟:

React原始碼解讀之componentMount

理解部分原始碼後那種喜悅的心情總是會隨時在你寫元件的時候伴隨著你,不過react留著的坑還有很多需要我去填補,我也會堅持不懈下去,最後恭喜法國隊贏得世界盃冠軍~

參考

深入React技術棧

知乎

CSDN

掘金

segfaultment

官網

github

相關文章