1 简介
React广受好评的一个重要原因就是组件化开发,一方面分模块的方式便于协同开发,降低耦合,后期维护也轻松;另一方面使得一次开发,多处复用成为现实,甚至可以直接复用开源React组件。开发完一个组件后,我们需要插入DOM中,一般使用如下代码
ReactDOM.render(
Hello, world!
,document.getElementById('example')
);
经过babel转码后为
ReactDOM.render(
React.createElement(
'h1', // type, DOM原生组件的type为string,React自定义组件type为Object
null, // config,会设置到ref,key,props中
'Hello, world!' // children,子组件.这儿为文本组件
),
document.getElementById('example')
)
那么React底层是怎么将组件插入DOM中的呢。本文来详细分析它的前因后果。
2 ReactMount._renderSubtreeIntoContainer()
ReactDOM.render()实际调用ReactMount.render(),接着调用到ReactMount._renderSubtreeIntoContainer().
这个调用链比较简单,不分析了。下面重点分析_renderSubtreeIntoContainer(). 我们去除掉开发调试阶段的报错代码(比如 “development” !== ‘production’)。
/**
* 将ReactElement插入DOM中,并返回ReactElement对应的ReactComponent。
* ReactElement是React元素在内存中的表示形式,可以理解为一个数据类,包含type,key,refs,props等成员变量
* ReactComponent是React元素的操作类,包含mountComponent(), updateComponent()等很多操作组件的方法
*
* @param {parentComponent} 父组件,对于第一次渲染,为null
* @param {nextElement} 要插入到DOM中的组件,对应上面例子中的
Hello, world!
经过babel转译后的元素* @param {container} 要插入到的容器,对应上面例子中的document.getElementById('example')获取的DOM对象
* @param {callback} 第一次渲染为null
*
* @return {component} 返回ReactComponent,对于ReactDOM.render()调用,不用管返回值。
*/
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
// 刚开始一段开发阶段的报错代码,省去
...
// 包装ReactElement,将nextElement挂载到wrapper的props属性下,这段代码不是很关键
var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
// 获取要插入到的容器的前一次的ReactComponent,这是为了做DOM diff
// 对于ReactDOM.render()调用,prevComponent为null
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
// 从prevComponent中获取到prevElement这个数据对象。一定要搞清楚ReactElement和ReactComponent的作用,他们很关键
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props;
// DOM diff精髓,同一层级内,type和key不变时,只用update就行。否则先unmount组件再mount组件
// 这是React为了避免递归太深,而做的DOM diff前提假设。它只对同一DOM层级,type相同,key(如果有)相同的组件做DOM diff,否则不用比较,直接先unmount再mount。这个假设使得diff算法复杂度从O(n^3)降低为O(n).
// shouldUpdateReactComponent源码请看后面的分析
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
// 只需要update,调用_updateRootComponent,然后直接return了
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
return publicInst;
} else {
// 不做update,直接先卸载再挂载。即unmountComponent,再mountComponent。mountComponent在后面代码中进行
ReactMount.unmountComponentAtNode(container);
}
}
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
var containerHasNonRootReactChild = hasNonRootReactChild(container);
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
// 初始化,渲染组件,然后插入到DOM中。_renderNewRootComponent很关键,后面详细分析
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
// render方法中带入的回调,ReactDOM.render()调用时一般不传入
if (callback) {
callback.call(component);
}
return component;
},
shouldUpdateReactComponent()源码如下:
function shouldUpdateReactComponent(prevElement, nextElement) {
// 前后两次ReactElement中任何一个为null,则必须另一个为null才返回true。这种情况一般不会碰到
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算法
if (prevType === 'string' || prevType === 'number') {
// 如果前后两次为数字或者字符,则认为只需要update(处理文本元素),返回true
return (nextType === 'string' || nextType === 'number');
} else {
// 如果前后两次为DOM元素或React元素,则必须type和key不变(key用于listView等组件,很多时候我们没有设置key,故只需type相同)才update,否则先unmount再重新mount。返回false
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
3.ReactMount._renderNewRootComponent
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
// 初始化ReactComponent,根据ReactElement中不同的type字段,创建不同类型的组件对象,即ReactComponent
// 前一篇文章中已经分析了。http://blog.csdn.net/u013510838/article/details/55669769
var componentInstance = instantiateReactComponent(nextElement);
// 处理batchedMountComponentIntoNode方法调用,将ReactComponent插入DOM中,后面详细分析
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
},
batchedMountComponentIntoNode以transaction事务的形式调用mountComponentIntoNode,源码分析如下。
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
var markerName;
// 一段log,可以不管
if (ReactFeatureFlags.logTopLevelRenders) {
var wrappedElement = wrapperInstance._currentElement.props;
var type = wrappedElement.type;
markerName = 'React mount: ' + (typeof type === 'string' ? type : type.displayName || type.name);
console.time(markerName);
}
// 调用对应ReactComponent中的mountComponent方法来渲染组件,这个是React生命周期的重要方法。后面详细分析。
// mountComponent返回React组件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多态
// 上面的
Hello, world!
, 对应的是ReactDOMTextComponent,最终解析成的HTML为//
Hello, world!
var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context);
if (markerName) {
console.timeEnd(markerName);
}
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
// 将解析出来的HTML插入DOM中
ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
}
_mountImageIntoNode源码如下
_mountImageIntoNode: function (markup, container, instance, shouldReuseMarkup, transaction) {
// 对于ReactDOM.render()调用,shouldReuseMarkup为false
if (shouldReuseMarkup) {
...
}
if (transaction.useCreateElement) {
// 清空container的子节点,这个地方不明白为什么这么做
while (container.lastChild) {
container.removeChild(container.lastChild);
}
DOMLazyTree.insertTreeBefore(container, markup, null);
} else {
// 将markup这个HTML设置到container这个DOM元素的innerHTML属性上,这样就插入到了DOM中了
setInnerHTML(container, markup);
// 将instance这个ReactComponent渲染后的对象,即Virtual DOM,保存到container这个DOM元素的firstChild这个原生节点上。简单理解就是将Virtual DOM保存到内存中,这样可以大大提高交互效率
ReactDOMComponentTree.precacheNode(instance, container.firstChild);
}
}
4 总结
ReactDOM.render()是渲染React组件并插入到DOM中的入口方法,它的执行流程大概为
1.React.createElement(),创建ReactElement对象。�他的重要的成员变量有type,key,ref,props。这个过程中会调用getInitialState(), 初始化state,只在挂载的时候才调用。后面update时不再调用了。
2.instantiateReactComponent(),根据ReactElement的type分别创建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等对象
3.mountComponent(), 调用React生命周期方法解析组件,得到它的HTML。
4._mountImageIntoNode(), 将HTML插入到DOM父节点中,通过设置DOM父节点的innerHTML属性。
5.缓存节点在React中的对应对象,即Virtual DOM。