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