我们上一篇文章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方法就是将一些危险性的内容替换为安全的,比如文本中有'<'转换为 '<' 最后返回一个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是根组件,一个项目中包含的内容定然是极其多的,而不是例子中那样,但是,逻辑上是一致的,只是遍历的次数多了一些,递归的程度深了一些而已。