React源码学习入门(九)DOM挂载细节流程
本文基于React v15.6.2版本介绍,原因请参见新手如何学习React源码
源码分析
React挂载DOM的核心流程在src/renderers/dom/shared/ReactDOMComponents.js下:
mountComponent: function(
transaction,
hostParent,
hostContainerInfo,
context,
) {
this._rootNodeID = globalIdCounter++;
this._domID = hostContainerInfo._idCounter++;
this._hostParent = hostParent;
this._hostContainerInfo = hostContainerInfo;
var props = this._currentElement.props;
var mountImage;
if (transaction.useCreateElement) {
var ownerDocument = hostContainerInfo._ownerDocument;
var el;
// 1. 创建一个DOM元素
if (namespaceURI === DOMNamespaces.html) {
if (this._tag === 'script') {
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 {
el = ownerDocument.createElement(this._currentElement.type);
}
} else {
el = ownerDocument.createElementNS(
namespaceURI,
this._currentElement.type,
);
}
// 2. precache DOM元素
ReactDOMComponentTree.precacheNode(this, el);
this._flags |= Flags.hasCachedChildNodes;
if (!this._hostParent) {
DOMPropertyOperations.setAttributeForRoot(el);
}
// 3. 更新DOM元素的Properties
this._updateDOMProperties(null, props, transaction);
var lazyTree = DOMLazyTree(el);
// 4. 创建children
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 + '>';
}
}
return mountImage;
},这里省略了一系列针对特殊DOM(如表单元素的Focus)处理,通用的DOM挂载实际上做了以下几件事:
创建对应的DOM元素(在之前的版本是通过字符串的拼接的方式,后面出于性能考虑改为createElement)
precache这个元素,便于在更新时候能够找到
更新DOM元素的Properties
创建children并挂载children(这是一个递归过程)
其中3和4就是DOM元素挂载的重头戏了,下面详细阐述一下:
更新DOM元素的Properties
这里更新Property的行为我们忽略了update的部分,只看初次挂载,update后续会专门分析,其实挂载的属性分为几种类型:
style标签,会处理空字符串的情况(删除CSS属性)
事件,在React中已注册的事件,以onXXX开头,这个是要交给事件中心去处理的
DOM本身的属性,通过setAttribute设置,同样处理了空属性的问题
创建并挂载children
第二个重点就是创建并挂载children的过程了,我们知道在React的JSX写法中,DOM元素的Child可以是React组件、DOM元素等等多种类型,所以这里理论上也是一个递归的过程,交给Reconciler的mount来重新处理:
这里的DOMLazyTree在前面介绍过,是为了更好地性能专门处理IE的问题,我们可以简单理解为queueText实际上就是设置textContent,queueChild实际上就是执行appendChild,queueHTML对应innerHTML。
这里的逻辑比较简单,大概归纳如下:
设置了
dangerouslySetInnerHTML.__html的,不管子元素,直接使用innerHTML覆盖子元素内容。如果子元素是一个字符串或者数字,那直接设置textContent
否则,遍历整个children,执行
mountChildren,并最终append到DOM上。
最后我们来看一看mountChildren,它的实现位于src/renderers/shared/reconciler/ReactMultiChild.js:
这里面做的事情非常简单,就两个步骤:
初始化每个child的实例(回忆一下我们在初始挂载的方法里也是做这个事情)
对每个child执行
mountComponent,拿到markup(images)列表返回
至此,整个DOM挂载的过程就结束了,生成的这个DOM节点最终通过之前我们提到的mountImage挂载到Container上。
小结一下
这里就是React实质挂载执行的叶子节点了,实际上也是DOM树真正形成的起点,当然结合之前我们提到的React组件的挂载流程,实际上就会发现最终能够挂载到DOM上的元素就是这里创建生成的DOM节点,或者是EmptyComponent的注释类节点和TextComponent生成的文本节点,整体DOM的挂载流程可以总结为下图:

Last updated