在【React-简单组件的挂载(mount)过程】的2.6)里提到了组件最后从
ReactElement
到HTML-DOM的转换过程,但是没展开。本周趁着还熟,这篇填坑。首先,简单画一下本篇的数据结构

dom-render
前面挂载一篇说过,从
ReactCompositeComponent
一步一步调用mountComponent
,最后会到最内层的ReactDomComponent.mountComponent
,本篇直接从这里往下面走。-ReactDomComponent.mountComponent
-ReactDOMComponentTree.precacheNode(this, el) // 创建双向链接,el = ownerDocument.createElement('div');
接着根据el信息创建用于渲染的主树(
lazyTree
)。-var lazyTree = DOMLazyTree(el);
至于这里为什么叫
lazyTree
?这部分的说明放到最后。实例化子节点
接着开始依次实例化子节点。并最后填充到主树上。
-this._createInitialChildren(transaction, props, context, lazyTree); // 填充主lazyTree
-this.mountChildren(childrenToUse, transaction, context); //childrenToUse == ['Hello', 'world', '!']
-ReactMultiChild._reconcilerInstantiateChildren // 实例化children
-ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context, selfDebugID);
traverseAllChildren // 前面一篇提到的递归,在这里
-instantiateChild
-instantiateReactComponent
-ReactHostComponent.createInstanceForText(node); // 依次实例化children节点
这一通操作之后,会取到最终实例化的子节点树,由一个个的
ReactDOMTextElement
组成。children = {
.0: ReactDOMTextElement{
"_currentElement": "Hello ",
"_stringText": "Hello ",
...
"_debugID": 3
}
.1: {"_currentElement": "World",...}
.2: {"_currentElement": "!",...}
}
-ReactReconciler.mountComponent // 对children循环调用
-ReactDOMTextComponent.mountComponent // 返回3个节点各自独立的子树
DOMLazyTree.queueChild(lazyTree, mountImages[0~2]); // 子树插入父级lazyTree
最后父级的lazyTree长这样
{
...
node: #document-fragment {
childNodes: { // NoeList
#comment-open:'react-text: 2'
#'Hello '
#comment-close: '/react-text'
...'world'...
...'!'...
}
}
...
}
如果用
f12
查看元素,会看到,最终的节点是由两段react-text
注释包起来的Hello
为什么要用
Comment
包起来?这部分也放在最后说。渲染浏览器DOM
在这一步操作之后,会逐层返回,最后调用最外层的
ReactMount._mountImageIntoNode(
markup, // 上一步的父级lazyTree
container, // #container DOM节点
wrapperInstance,
shouldReuseMarkup,
transaction
);
这里分为2个小步骤:
removeChild
递归清空container
容器内容;- 在
#container
节点上调用insertBefore
插入lazyTree.node
。
这样,到浏览器的DOM就渲染完成了。
小记
1、为什么需要lazyTree?
lazyTree
主要解决的是在IE(8-11)和Edge浏览器中,插入节点的效率问题。总的来说,在上述IE系列浏览器中,一个一个插入无子孙的节点,效率要远高于插入一整个序列化完整的节点树。关于二者的效率差异和说明,可以看这里:innerHTML vs. createElement vs. cloneNode
具体来说,在上述IE系列里,从最“孙子”的节点开始往上,挨个
parentNode.insertBefore
,会比较快。在非浏览器里,可以从下网上,parentNode.appendChild
,效率也很高。差别很明显,一个自顶向下,一个自下向上。所以可以看到lazyTree生成的结构是这样lazyTree = {
node: el,
children: [],
html: null,
text: null,
toString: toString
}
这个结构里,
node
用于非IE浏览器的DOM操作,里面直接就是搞好的HTML节点。而children
用于IE系列,因为需要记录下来回去的路径,才能遍历。说明二者操作差异的函数,在
DOMLazyTree.queueChild
function queueChild(parentTree, childTree) {
if (enableLazy) {
parentTree.children.push(childTree);
} else {
parentTree.node.appendChild(childTree.node);
}
}
最后
insertTreeBefore
,会判断是操作children
插入节点,还是直接插入node
。2、为什么要用
Comment
把text
包起来?答案是避免不必要的遍历。
前面可以看到,每一个
ReactDOMTextComponent
都包含一个指向Comment
的链接,反向链接也由。这就相当于从虚拟节点到实际节点的双向链接,以保证每个节点都是立即可达的。但是,单纯的一段
text
,没有办法添加附加属性来创建反向链接,所以这里使用了Comment
来承载节点信息。关于Comment的创建,使用的是
Document.createComment()
,可能这个方法不常用,具体说明可看MDN: createComment。