React-简单组件到浏览器DOM的渲染

2018年12月30日 ... ☕️ 3 min read

在【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注释包起来的

<!-- react-text: 2 -->
Hello 
<!-- /react-text -->

为什么要用Comment包起来?这部分也放在最后说。

渲染浏览器DOM 在这一步操作之后,会逐层返回,最后调用最外层的

ReactMount._mountImageIntoNode(
	markup, // 上一步的父级lazyTree
	container, // #container DOM节点
	wrapperInstance,
	shouldReuseMarkup,
	transaction
);

这里分为2个小步骤:

  1. removeChild递归清空container容器内容;
  2. 在#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。

#react

SideEffect is a blog for front-end web development.
Code by Axiu / rss