概述
上一篇講到React
中的元素(ReactElement
的“實(shí)例”)會(huì)有一個(gè)type
屬性,而該值將決定其被渲染時(shí)的處理結(jié)果合呐。
ReactDOM.render
實(shí)際即為React初次將vdom渲染至真實(shí)dom樹的過程暮的,其中包括了創(chuàng)建元素、添加屬性淌实、綁定事件等等操作冻辩。
本篇猖腕,我們就通過ReactDOM.render
的源碼來了解一下其處理過程。
ReactDOM.render方法使用
首先看ReactDOM.render
的使用方式:
const App = (<h2>Hello World!</h2>)
ReactDOM.render(App, document.querySelector('#app'))
或者
class App extends React.Component {
render(){
return (
<div>
<h2>Hello World!</h2>
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector('#app'))
根據(jù)我們上一篇的討論恨闪,我們知道上面兩個(gè)例子中ReactDOM.render
第一個(gè)參數(shù)傳入的都是ReactElement
的“實(shí)例”倘感。
而當(dāng)?shù)谝粋€(gè)參數(shù)傳入一個(gè)字符串類型,如下:
ReactDOM.render('This is String', document.querySelector('#app'))
// Uncaught Error: ReactDOM.render(): Invalid component element. Instead of passing a string like 'div', pass React.createElement('div') or <div />.
可見咙咽,ReactDOM.render第一個(gè)參數(shù)不支持字符串類型老玛,即不會(huì)直接創(chuàng)建 TextNode 插入到第二個(gè)參數(shù)指定的容器中。
接下來钧敞,我們一起進(jìn)入到源碼中查看該方法蜡豹。
源碼結(jié)構(gòu)
查看ReactDOM.js
文件,可以看到ReactDOM.render
引用ReactMount.js
的render
方法,如下:
ReactMount = {
// ReactDOM.render直接引用此方法
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},
// 實(shí)際執(zhí)行render的方法
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
// 將傳入的element用TopLevelWrapper包裝犁享,
// 包裝后的元素余素,標(biāo)記有rootID,并且擁有render方法炊昆,
// 具體可看TopLevelWrapper的源碼
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement
});
// ReactDOM.render方法調(diào)用時(shí),parentComponent為null
var nextContext;
if (parentComponent) {
var parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}
// 第一次執(zhí)行時(shí)威根,prevComponent為null凤巨,具體可看此方法源碼
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props.child;
// 判斷上一次的prevElement和nextElement是否是同一個(gè)組件,或者僅僅是數(shù)字洛搀、字符串敢茁,如果是,則直接update留美,
// 否則彰檬,重新渲染整個(gè)Element
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
// 更新vdom
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
return publicInst;
} else {
ReactMount.unmountComponentAtNode(container);
}
}
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
var containerHasNonRootReactChild = hasNonRootReactChild(container);
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
// 本次為首次渲染,因此調(diào)用ReactMount._renderNewRootComponent
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
},
/**
* Render a new component into the DOM. Hooked by hooks!
*
* @param {ReactElement} nextElement element to render
* @param {DOMElement} container container to render into
* @param {boolean} shouldReuseMarkup if we should skip the markup insertion
* @return {ReactComponent} nextComponent
*/
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
// 初始化組件實(shí)例谎砾,并增加組件掛載(mount)逢倍、更新(update)、卸載(unmount)等方法
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);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
},
}
從以上代碼可以看出景图,當(dāng)調(diào)用ReactDOM.render
時(shí)较雕,使用TopLevelWrapper
對element進(jìn)行包裝,隨后將其傳入ReactMount._renderNewRootComponent
中挚币,在此方法內(nèi)亮蒋,調(diào)用instantiateReactComponent
組件的實(shí)例,該實(shí)例擁有mountComponent
等掛載妆毕、更新的方法慎玖。
接下來學(xué)習(xí)instantiateReactComponent
的源碼,源碼位置位于instantiateReactComponent.js
文件笛粘。
/**
* Given a ReactNode, create an instance that will actually be mounted.
*
* @param {ReactNode} node
* @param {boolean} shouldHaveDebugID
* @return {object} A new instance of the element's constructor.
* @protected
*/
function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
var type = element.type;
// 代碼塊(1)
// Special case string values
if (typeof element.type === 'string') {
// type為string的趁怔,調(diào)用createInternalComponent方法远舅,
// 對節(jié)點(diǎn)進(jìn)行處理衰齐,包含屬性柏副、默認(rèn)事件等等
instance = ReactHostComponent.createInternalComponent(element); // (2)
} else if (isInternalComponentType(element.type)) {
// 內(nèi)置type?
// This is temporarily available for custom components that are not string
// representations. I.e. ART. Once those are updated to use the string
// representation, we can drop this code path.
instance = new element.type(element);
// We renamed this. Allow the old name for compat. :(
if (!instance.getHostNode) {
instance.getHostNode = instance.getNativeNode;
}
} else {
// 其余的均為自定義組件, 通過此方法报破,創(chuàng)建組件實(shí)例
// 此方法比較復(fù)雜
instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
// 字符串或數(shù)字任连,直接調(diào)用 createInstanceForText蚤吹,生成實(shí)例
instance = ReactHostComponent.createInstanceForText(node);
} else {
!false ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Encountered invalid React node of type %s', typeof node) : _prodInvariant('131', typeof node) : void 0;
}
// These two fields are used by the DOM and ART diffing algorithms
// respectively. Instead of using expandos on components, we should be
// storing the state needed by the diffing algorithms elsewhere.
// 與diff算法相關(guān),TOREAD...
instance._mountIndex = 0;
instance._mountImage = null;
return instance;
}
結(jié)合注釋細(xì)讀以上代碼随抠,如代碼塊(1)中裁着,根據(jù)node
的type
類型來渲染節(jié)點(diǎn),也即本文一開始所提到的type
拱她。為更好理解二驰,我們使用以下代碼渲染一個(gè)input元素:
/**
* 以下JSX相當(dāng)于:
* const inputEle = React.createElement('input', {
* defaultValue: '10',
* onClick: () => console.log('clicked')
* })
*/
const inputEle = (
<input
defaultValue="10"
onClick={() => console.log('clicked')}
/>
)
ReactDOM.render(inputEle, document.getElementById('app'))
根據(jù)我們上一篇所講,inputEle
為ReactElement
的一個(gè)實(shí)例秉沼,其type
屬性為input
桶雀。
因此,在instantiateReactComponent
方法中唬复,應(yīng)該執(zhí)行(2)處的分支矗积,即:ReactHostComponent.createInternalComponent(element)
。
我們查看ReactHostComponent.js
文件敞咧,可看到createInternalComponent
方法棘捣,代碼如下:
/**
* Get a host internal component class for a specific tag.
*
* @param {ReactElement} element The element to create.
* @return {function} The internal class constructor function.
*/
function createInternalComponent(element) {
!genericComponentClass ? process.env.NODE_ENV !== 'production' ? invariant(false, 'There is no registered component for the tag %s', element.type) : _prodInvariant('111', element.type) : void 0;
return new genericComponentClass(element);
}
即返回genericComponentClass
的一個(gè)實(shí)例,而genericComponentClass
的來源休建,追尋源碼乍恐,可以找到在ReactDefaultInjection
中找到,實(shí)際上將ReactDOMComponent
注入進(jìn)來测砂。
ReactDOM源碼中茵烈,作者將各種類型(如ReactEventListener、ReactDOMComponent等)抽象后通過Injection機(jī)制注入邑彪,我的理解是這樣方便未來將類型整體升級(jí)替換瞧毙,并且能一定程度上解耦(只需要保證類型對外提供的接口一致)。不知道是否理解有誤... ...還望指教寄症。
因此instantiateReactComponent
的代碼(2)處實(shí)際返回:new ReactDOMComponent(node)
宙彪。
接下來閱讀ReactDOMComponent.js
文件:
先看ReactDOMComponent
這個(gè)方法:
/**
* Creates a new React class that is idempotent and capable of containing other
* React components. It accepts event listeners and DOM properties that are
* valid according to `DOMProperty`.
*
* - Event listeners: `onClick`, `onMouseDown`, etc.
* - DOM properties: `className`, `name`, `title`, etc.
*
* The `style` property functions differently from the DOM API. It accepts an
* object mapping of style properties to values.
*
* @constructor ReactDOMComponent
* @extends ReactMultiChild
*/
function ReactDOMComponent(element) {
var tag = element.type;
validateDangerousTag(tag);
this._currentElement = element;
this._tag = tag.toLowerCase();
this._namespaceURI = null;
this._renderedChildren = null;
this._previousStyle = null;
this._previousStyleCopy = null;
this._hostNode = null;
this._hostParent = null;
this._rootNodeID = 0;
this._domID = 0;
this._hostContainerInfo = null;
this._wrapperState = null;
this._topLevelWrapper = null;
this._flags = 0;
if (process.env.NODE_ENV !== 'production') {
this._ancestorInfo = null;
setAndValidateContentChildDev.call(this, null);
}
}
_assign(ReactDOMComponent.prototype, ReactDOMComponent.Mixin, ReactMultiChild.Mixin)
以上代碼可以看到,ReactDOMComponent
這個(gè)類繼承了ReactMultiChild
的Mixin
有巧。
元素掛載時(shí)释漆,實(shí)際調(diào)用:ReactDOMComponent.Mixin
中的mountComponent
方法,整體源碼如下:
/**
* Generates root tag markup then recurses. This method has side effects and
* is not idempotent.
*
* @internal
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {?ReactDOMComponent} the parent component instance
* @param {?object} info about the host container
* @param {object} context
* @return {string} The computed markup.
*/
mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
this._rootNodeID = globalIdCounter++;
this._domID = hostContainerInfo._idCounter++;
this._hostParent = hostParent;
this._hostContainerInfo = hostContainerInfo;
var props = this._currentElement.props;
// 調(diào)整props至DOM的合法屬性,并且處理事件
switch (this._tag) {
case 'audio':
case 'form':
case 'iframe':
case 'img':
case 'link':
case 'object':
case 'source':
case 'video':
this._wrapperState = {
listeners: null
};
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'input':
ReactDOMInput.mountWrapper(this, props, hostParent);
props = ReactDOMInput.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'option':
ReactDOMOption.mountWrapper(this, props, hostParent);
props = ReactDOMOption.getHostProps(this, props);
break;
case 'select':
ReactDOMSelect.mountWrapper(this, props, hostParent);
props = ReactDOMSelect.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
case 'textarea':
ReactDOMTextarea.mountWrapper(this, props, hostParent);
props = ReactDOMTextarea.getHostProps(this, props);
transaction.getReactMountReady().enqueue(trackInputValue, this);
transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
break;
}
assertValidProps(this, props);
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
var namespaceURI;
var parentTag;
if (hostParent != null) {
namespaceURI = hostParent._namespaceURI;
parentTag = hostParent._tag;
} else if (hostContainerInfo._tag) {
namespaceURI = hostContainerInfo._namespaceURI;
parentTag = hostContainerInfo._tag;
}
if (namespaceURI == null || namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
namespaceURI = DOMNamespaces.html;
}
if (namespaceURI === DOMNamespaces.html) {
if (this._tag === 'svg') {
namespaceURI = DOMNamespaces.svg;
} else if (this._tag === 'math') {
namespaceURI = DOMNamespaces.mathml;
}
}
this._namespaceURI = namespaceURI;
var mountImage;
if (transaction.useCreateElement) {
var ownerDocument = hostContainerInfo._ownerDocument;
var el;
if (namespaceURI === DOMNamespaces.html) {
if (this._tag === 'script') {
// Create the script via .innerHTML so its "parser-inserted" flag is
// set to true and it does not execute
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);
}
ReactDOMComponentTree.precacheNode(this, el);
this._flags |= Flags.hasCachedChildNodes;
if (!this._hostParent) {
DOMPropertyOperations.setAttributeForRoot(el);
}
this._updateDOMProperties(null, props, transaction);
var lazyTree = DOMLazyTree(el);
this._createInitialChildren(transaction, props, context, lazyTree);
mountImage = lazyTree;
} else {
var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
var tagContent = this._createContentMarkup(transaction, props, context);
if (!tagContent && omittedCloseTags[this._tag]) {
mountImage = tagOpen + '/>';
} else {
mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';
}
}
switch (this._tag) {
case 'input':
transaction.getReactMountReady().enqueue(inputPostMount, this);
if (props.autoFocus) {
transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'textarea':
transaction.getReactMountReady().enqueue(textareaPostMount, this);
if (props.autoFocus) {
transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'select':
if (props.autoFocus) {
transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'button':
if (props.autoFocus) {
transaction.getReactMountReady().enqueue(AutoFocusUtils.focusDOMComponent, this);
}
break;
case 'option':
transaction.getReactMountReady().enqueue(optionPostMount, this);
break;
}
return mountImage;
}
閱讀上述代碼篮迎,可以知道React是如何將一個(gè)ReactElement與DOM進(jìn)行映射的(本例子只展示了DOMComponent這種類型男图,自定義組件示姿、textNode這兩種可自行找到源碼閱讀)。
上述方法返回的值將會(huì)被傳入ReactUpdates.batchedUpdates
中進(jìn)行掛載逊笆,這部分內(nèi)容較為復(fù)雜栈戳,在未來將進(jìn)一步解讀。