React源码学习系列(二)——|React源码学习系列(二)—— ReactDOM.render,初次渲染

概述 上一篇讲到React中的元素(ReactElement的“实例”)会有一个type属性,而该值将决定其被渲染时的处理结果。
ReactDOM.render实际即为React初次将vdom渲染至真实dom树的过程,其中包括了创建元素、添加属性、绑定事件等等操作。
本篇,我们就通过ReactDOM.render的源码来了解一下其处理过程。
ReactDOM.render方法使用 首先看ReactDOM.render的使用方式:

const App = (Hello World!) ReactDOM.render(App, document.querySelector('#app'))

或者
class App extends React.Component { render(){ return (Hello World!) } } ReactDOM.render(, document.querySelector('#app'))

根据我们上一篇的讨论,我们知道上面两个例子中ReactDOM.render第一个参数传入的都是ReactElement的“实例”。
而当第一个参数传入一个字符串类型,如下:
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 .

可见,ReactDOM.render第一个参数不支持字符串类型,即不会直接创建 TextNode 插入到第二个参数指定的容器中。
【React源码学习系列(二)——|React源码学习系列(二)—— ReactDOM.render,初次渲染】接下来,我们一起进入到源码中查看该方法。
源码结构 查看ReactDOM.js文件,可以看到ReactDOM.render引用ReactMount.jsrender方法,如下:
ReactMount = { // ReactDOM.render直接引用此方法 render: function (nextElement, container, callback) { return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback); }, // 实际执行render的方法 _renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) { ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render'); // 将传入的element用TopLevelWrapper包装, // 包装后的元素,标记有rootID,并且拥有render方法, // 具体可看TopLevelWrapper的源码 var nextWrappedElement = React.createElement(TopLevelWrapper, { child: nextElement }); // ReactDOM.render方法调用时,parentComponent为null var nextContext; if (parentComponent) { var parentInst = ReactInstanceMap.get(parentComponent); nextContext = parentInst._processChildContext(parentInst._context); } else { nextContext = emptyObject; }// 第一次执行时,prevComponent为null,具体可看此方法源码 var prevComponent = getTopLevelWrapperInContainer(container); if (prevComponent) { var prevWrappedElement = prevComponent._currentElement; var prevElement = prevWrappedElement.props.child; // 判断上一次的prevElement和nextElement是否是同一个组件,或者仅仅是数字、字符串,如果是,则直接update, // 否则,重新渲染整个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; // 本次为首次渲染,因此调用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(); // 初始化组件实例,并增加组件挂载(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; }, }

从以上代码可以看出,当调用ReactDOM.render时,使用TopLevelWrapper对element进行包装,随后将其传入ReactMount._renderNewRootComponent中,在此方法内,调用instantiateReactComponent组件的实例,该实例拥有mountComponent等挂载、更新的方法。
接下来学习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的,调用createInternalComponent方法, // 对节点进行处理,包含属性、默认事件等等 instance = ReactHostComponent.createInternalComponent(element); // (2) } else if (isInternalComponentType(element.type)) { // 内置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 { // 其余的均为自定义组件, 通过此方法,创建组件实例 // 此方法比较复杂 instance = new ReactCompositeComponentWrapper(element); } } else if (typeof node === 'string' || typeof node === 'number') { // 字符串或数字,直接调用 createInstanceForText,生成实例 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算法相关,TOREAD... instance._mountIndex = 0; instance._mountImage = null; return instance; }

结合注释细读以上代码,如代码块(1)中,根据nodetype类型来渲染节点,也即本文一开始所提到的type。为更好理解,我们使用以下代码渲染一个input元素:
/** * 以下JSX相当于: * const inputEle = React.createElement('input', { *defaultValue: '10', *onClick: () => console.log('clicked') * }) */ const inputEle = ( console.log('clicked')} /> )ReactDOM.render(inputEle, document.getElementById('app'))

根据我们上一篇所讲,inputEleReactElement的一个实例,其type属性为input
因此,在instantiateReactComponent方法中,应该执行(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的一个实例,而genericComponentClass的来源,追寻源码,可以找到在ReactDefaultInjection中找到,实际上将ReactDOMComponent注入进来。
ReactDOM源码中,作者将各种类型(如ReactEventListener、ReactDOMComponent等)抽象后通过Injection机制注入,我的理解是这样方便未来将类型整体升级替换,并且能一定程度上解耦(只需要保证类型对外提供的接口一致)。不知道是否理解有误... ...还望指教。
因此instantiateReactComponent的代码(2)处实际返回:new ReactDOMComponent(node)
接下来阅读ReactDOMComponent.js文件:
先看ReactDOMComponent这个方法:
/** * 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这个类继承了ReactMultiChildMixin
元素挂载时,实际调用: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; // 调整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 + '>'; 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 + ''; } }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是如何将一个ReactElement与DOM进行映射的(本例子只展示了DOMComponent这种类型,自定义组件、textNode这两种可自行找到源码阅读)。
上述方法返回的值将会被传入ReactUpdates.batchedUpdates中进行挂载,这部分内容较为复杂,在未来将进一步解读。

    推荐阅读