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挂载实际上做了以下几件事:

  1. 创建对应的DOM元素(在之前的版本是通过字符串的拼接的方式,后面出于性能考虑改为createElement)

  2. precache这个元素,便于在更新时候能够找到

  3. 更新DOM元素的Properties

  4. 创建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实际上就是设置textContentqueueChild实际上就是执行appendChildqueueHTML对应innerHTML

这里的逻辑比较简单,大概归纳如下:

  • 设置了dangerouslySetInnerHTML.__html的,不管子元素,直接使用innerHTML覆盖子元素内容。

  • 如果子元素是一个字符串或者数字,那直接设置textContent

  • 否则,遍历整个children,执行mountChildren,并最终append到DOM上。

最后我们来看一看mountChildren,它的实现位于src/renderers/shared/reconciler/ReactMultiChild.js

这里面做的事情非常简单,就两个步骤:

  1. 初始化每个child的实例(回忆一下我们在初始挂载的方法里也是做这个事情)

  2. 对每个child执行mountComponent,拿到markup(images)列表返回

至此,整个DOM挂载的过程就结束了,生成的这个DOM节点最终通过之前我们提到的mountImage挂载到Container上。

小结一下

这里就是React实质挂载执行的叶子节点了,实际上也是DOM树真正形成的起点,当然结合之前我们提到的React组件的挂载流程,实际上就会发现最终能够挂载到DOM上的元素就是这里创建生成的DOM节点,或者是EmptyComponent的注释类节点和TextComponent生成的文本节点,整体DOM的挂载流程可以总结为下图:

Last updated