ReactDOM.render做了什么 -- 生成markup

325 阅读8分钟

我们上一篇文章mount-- 第三部分说过,调用 ReactReconciler.mountComponent 方法生成了markup。最后,直接将 markup当做 innerHtMl属性的值给了container元素,来将组件挂载。

这里我们来探讨一下,markup 是如何生成的。

这里需要用到 Element部分的知识,不熟悉的可以往前翻一下文章看看。

ReactReconciler.mountComponent

位于 ReactReconciler 模块的 mountComponent方法如下所示:

    mountComponent: function (internalInstance, rootID, transaction, context) {
        var markup = internalInstance.mountComponent(rootID, transaction, context);
        if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
          transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
        }
        return markup;
      },

从上边的代码中我们可以看出来,生成markup核心是靠 internalInstance.mountComponent方法。而internalInstance则是组件的挂载实例。也就是调用 instantiateReactComponent 方法初始化了 Element所展现的值。

每一个 组件实例都会有其自身的 mountComponent方法。我们知道,组件实例主要分为三类。

三种组件实例

一:自定义组件生成的实例--- 也就是ReactCompositeComponentWrapper的实例

二:是html元素生成的实例-- 也就是 ReactDOMComponent 的实例

三:是纯文本生成的实例 -- 也就是 ReactDOMTextComponent 的实例

PS:这里不考虑 组件render的内容为null或者是false的情况。

举个🌰

我们以下图中的App组件作为根组件

React要挂载这个组件要做的事情是执行 这篇文章的内容 也就是组件挂载的内容。具体的操作就不赘述了,不了解的可以返回去看看。 这里简单的罗列了一下,React的挂载步骤

一:

调用下边的代码,将App组件挂载在id为app的div内

    ReactDOM.render(<App />, document.getElementById("app"));

二:

具体的处理逻辑是调用了 _renderSubtreeIntoContainer 方法。

    render: function (nextElement, container, callback) {
        return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
    },

三:

这个方法是 _renderSubtreeIntoContainer,主要是做了下边两件事。

第一是生成了 nextWrappedElement,也就是id为app的div的Element表示形式。 第二是调用了_renderNewRootComponent 方法,nextWrappedElement是其参数。

    var nextWrappedElement = new ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
    
    。。。
    
     ReactMount._renderNewRootComponent(
      nextWrappedElement, ...)

四 _renderNewRootComponent:

第一是将传入的 nextWrappedElement 实例化,命名为 componentInstance。

第二是 使用批处理调用 batchedMountComponentIntoNode方法

    var componentInstance = instantiateReactComponent(nextElement, null);
    
    。。。
    
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, reactRootID, container, shouldReuseMarkup, context);

五:batchedMountComponentIntoNode

主要是使用事务 调用了方法 mountComponentIntoNode,参数为 componentInstance等

    transaction.perform(mountComponentIntoNode, null, componentInstance, rootID, container, transaction, shouldReuseMarkup, context);

六 mountComponentIntoNode

调用了 ReactReconciler.mountComponent生产 markup。

     var markup = ReactReconciler.mountComponent(componentInstance, rootID, transaction, context);

到此,我们知道 componentInstance 是 nextWrappedElement的实例

七 看看 nextWrappedElement 与实例

nextWrappedElement 长这样:

nextWrappedElement 的实例长这样:

无需怀疑,nextWrappedElement 实例是一个特殊的 ReactCompositeComponent 类的实例。而且,nextWrappedElement 也是采用 ReactCompositeComponent 类来实例化的。

八 ReactReconciler.mountComponent

我们接着上边的从 ReactReconciler.mountComponent 方法开始说起。

     var markup = internalInstance.mountComponent(rootID, transaction, context);

internalInstance是 nextWrappedElement 实例化之后的实例,实例化采用的是ReactCompositeComponent类。 也就是调用了ReactCompositeComponent类的mountComponent方法。可以参考这篇文章

调用实例的 mountComponent方法主要是做了下边这些事情:

    if (renderedElement === undefined) {
        renderedElement = this._renderValidatedComponent();
    }

一:初始情况下 renderedElement 肯定是undefined的。这个时候为 renderedElement 赋值为 this._renderValidatedComponent();

this._renderValidatedComponent() 这个方法最终会调用 this(也就是当前的组件实例)的render方法。

nextWrappedElement 实例的render方法是返回 其自身的props属性。也就是App组件 的Element形式 (nextWrappedElement的render方法定义在ReactMount模块的 TopLevelWrapper方法的原型上定义)

renderedElement nextWrappedElement 实例化之后的props值,也就是App组件的 Element,我们称呼它为 AppElement

这里对 AppElement 继续对其实例化,实例化的AppElement我们称它为 AppInstance


    this._renderedComponent = this._instantiateReactComponent(renderedElement);
    

看一下AppInstance,如下所示:

这里再次调用了ReactReconciler.mountComponent 方法,只是参数变成了 AppInstance

     var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));

ReactReconciler.mountComponent方法是调用了组件实例的 mountComponent发光法。这组件实例是 AppInstance,而 AppInstance实例也是 ReactCompositeComponent 类的实例。那么 mountComponent 方法 是 继承自 ReactCompositeComponent类的。

    var markup = internalInstance.mountComponent(rootID, transaction, context);

renderedElement此时依旧是undefined, 所以 renderedElement 就是 App组件的render所返回的 Element。

    if (renderedElement === undefined) {
        renderedElement = this._renderValidatedComponent();
    }

我们来看看这时候的 renderedElement 是什么

如此我们称它为 AppChildElement

    this._renderedComponent = this._instantiateReactComponent(renderedElement);

这里将 AppChildElement 实例化,实例化的结果如下。

这里注意一点:此时 AppChildElement.type为div是字符串。实例化的类为 ReactDOMComponent。实例化的组件我们称之为 AppChildInstance

将 AppChildInstance 作为参数,调用 ReactReconciler.mountComponent方法。

     var markup = ReactReconciler.mountComponent(this._renderedComponent, rootID, transaction, this._processChildContext(context));

调用 AppChildInstance 的 mountComponent 方法。

    var markup = internalInstance.mountComponent(rootID, transaction, context);

这里,我之前以为会造成死循环,就是一致调用下去,后来才发现,这个时候的mountComponent方法是 使用 ReactDOMComponent 类的 mountComponent 方法,而不是上边的 ReactCompositeComponent 类的 mountComponent 方法。 关于 ReactDOMComponent的 mountComponent 方法参考文章

我们继续来看 ReactDOMComponent的 mountComponent方法。 tagOpen 就是 html元素左侧部分这一部分里包含了元素的属性等信息 比如:

    <div id="first" style="color:blue" onClick="xxxx"

如上这些便是 tagOpen,tagOpen 就是根据 AppChildElement 的type来建一个元素。一个开口的标签,如上所示的那样

    var tagOpen =this._createOpenTagMarkupAndPutListeners(transaction, props);
    

🔟

主要的是 tagContent。tagContent就是 App组件的内容。比如例子中所示的那样。也就是 AppChildElement 的props属性的,children里的值。 这一部分是 Element创建的时候的内容参考内容

11

再来看看 tagContent

    var tagContent = this._createContentMarkup(transaction, props, context);
    

12

调用了 _createContentMarkup 方法。

_createContentMarkup方法的核心是调用了mountChildren

childrenToUse是 AppChildElement 的属性props.children,
也就是:
[
"this is app ",
 13
]
    var mountImages = this.mountChildren(childrenToUse, transaction, context);

13

_createContentMarkup方法的核心是调用了mountChildren方法。 看看 mountChildren 方法,在 ReactMultiChild模块中 而,mountChildren方法过长,我们将其拆开,第一部分的内容就是将组件的所有子元素都进行实例化。

13 - 1 mountChildren 第一部分

这里将 nestedChildren,也就是 [ "this is app ",13] 实例化,

    mountChildren: function (nestedChildren, transaction, context) {
       
      var children = this._reconcilerInstantiateChildren(nestedChildren, transaction, context);
      
     
    },
13-1-1_reconcilerInstantiateChildren 方法

该方法调用了 ReactChildReconciler.instantiateChildren 方法。

    _reconcilerInstantiateChildren: function (nestedChildren, transaction, context) {
        return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
    }
    
13-1-2

ReactChildReconciler.instantiateChildren 方法返回了一个对象childInstances,这个对象里存储的是

[ "this is app ", 13 ] 的组件实例,具体将其实例化的方法是 traverseAllChildren文件的 traverseAllChildren。方法。

    instantiateChildren: function (nestedChildNodes, transaction, context) {
        if (nestedChildNodes == null) {
          return null;
        }
        var childInstances = {};
        traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
        return childInstances;
    },
13-1-3 traverseAllChildren

traverseAllChildren方法只是调用了 traverseAllChildrenImpl方法,

    function traverseAllChildren(children, callback, traverseContext) {
      if (children == null) {
        return 0;
      }
      
      return traverseAllChildrenImpl(children, '', callback, traverseContext);
    }
13-1-4 traverseAllChildrenImpl

callback是传递的参数吗,也就是 instantiateChild 方法。traverseContext则是 instantiateChildren方法的 childInstances,用来存储App组件的子元素的实例的。

   if (children === null || type === 'string' || type === 'number' || ReactElement.isValidElement(children)) {
   
        callback(traverseContext, children,
     
        nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
        return 1;
  }
13-1-5 instantiateChild

来看看 instantiateChild方法,在 ReactChildReconciler模块中

这个方法则是对每一个 组件的子元素进行 实例化,也就是遍历 ['this is app',13],而后 调用了 instantiateReactComponent方法。实例化之后的实例存入 childInstances中,也就是方法 instantiateChildren 的 childInstances 对象内。

    function instantiateChild(childInstances, child, name) {
    
      var keyUnique = childInstances[name] === undefined;
     
      if (child != null && keyUnique) {
        childInstances[name] = instantiateReactComponent(child, null);
      }
}
    

至此,mountChildren方法的第一部分完成,App组件的所有子元素,'this is app' 和 13 两个元素被实例化。 如下所示:

13-2 mountChildren第二部分

    mountChildren: function (nestedChildren, transaction, context) {
        this._renderedChildren = children;
        var mountImages = [];
        var index = 0;
        for (var name in children) {
            if (children.hasOwnProperty(name)) {
              var child = children[name];
              var rootID = this._rootNodeID + name;
              var mountImage = ReactReconciler.mountComponent(child, rootID, transaction, context);
              child._mountIndex = index++;
              mountImages.push(mountImage);
            }
        }
        return mountImages;
        
    }
    
13-2-1

遍历去找 children 内的每一个组件的 mountImage。 也就是调用 ReactReconciler.mountComponent 方法

    mountComponent: function (internalInstance, rootID, transaction, context) {
        var markup = internalInstance.mountComponent(rootID, transaction, context);
        if (internalInstance._currentElement && internalInstance._currentElement.ref != null) {
          transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
        }
        return markup;
    },
13-2-2

ReactReconciler.mountComponent方法实际是调用了 children 内的每一个 实例的mountComponent方法。 从上边我们知道。children内有两个实例,且都是 ReactDOMTextComponent 类型的。所以就是调用 ReactDOMTextComponent 类的 mountComponent方法。

    var markup = internalInstance.mountComponent(rootID, transaction, context);
13-2-3 ReactDOMTextComponent.mountComponent

_stringText属性是 就是 文本 也就是 'this is app' 或者是 13

这里的 escapeTextContentForBrowser方法就是将一些危险性的内容替换为安全的,比如文本中有'<'转换为 '&lt' 最后返回一个span。

    var escapedText = escapeTextContentForBrowser(this._stringText);

    return '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' + escapedText + '</span>';

这里说一下,纯文本的组件参考文章 纯文本的组件React会将它们全都包一层span标签。 如例子中 App组件的 'this is app' 这段文字没有使用HMl标签包裹,所以它就属于 纯文本。

13-2-4 mountImages

mountImages 是一个数组,放入的内容是

    ["<span data-reactid=".0.0">this is app </span>", "<span data-reactid=".0.1">13</span>"]

13 mountChildren 方法总结

到此,mountChildren 方法返回了一个数组,mountImages,其内放置的是 拼接好的markup。

因为我们选择的 app组件的子元素都是纯文本的,而app的子元素可以是任何的html元素,也可以是自定义组件。

如果有html元素存在,同样的会将这个html元素实例化为 ReactDOMComponent 的实例。而后调用其mountComponent方法生成markup而后放入 mountChildren 中。

当然,如果 App组件的子元素是一个自定义的组件,那么同样的,将其初始化为 ReactCompositeComponent类的实例。而后调用实例的 mountComponent方法,而调用一个自定义组件的实例的 mountComponent方法会产生递归调用将自定义组件层层剥开而后实例化,直到,最后的是 ReactDOMComponent 的实例或者是 ReactDOMTextComponent的实例为止,而后调用实例的 mountComponent方法,生成markup放入 mountChildren 中。

14 最终章 生成 mountImage

上边说了那么多其核心是造就了 tagContent。也就是App组件的内元素的html表示 _createContentMarkup返回的便是App组件jsx部分的html表示形式。

    mountImage = tagOpen + '>' + tagContent + '</' + this._currentElement.type + '>';

上边例子所产生的 mountImage则为下边的:

    <div data-reactid=".0">
        <span data-reactid=".0.0">this is app </span>
        <span data-reactid=".0.1">13</span>
    </div>

最后将其拼接为一个 mountImage返回。至此,App组件的表示形式便是 mountIage。这里的App是根组件,一个项目中包含的内容定然是极其多的,而不是例子中那样,但是,逻辑上是一致的,只是遍历的次数多了一些,递归的程度深了一些而已。