Last time we completed the upper half of the class component rendering logic which is similar to albeit different from simple component rendering in the following respects: 1) it instantiated one additional ReactCompositeComponent
to
represent the class component (App
); and 2) it calls App.render()
that triggers cascading React.createElement()
s to establish a ReactElement
tree.
This time we are going to explore more branches in lower half by examining how the ReactElements
in the tree is transformed to their respective ReactDOMComponents
, and eventually, to real DOM objects.
Files used in this article:
renderers/dom/shared/ReactDOMComponent.js: offers the two methods that this post focuses on, mountChildren()
and _createInitialChildren
renderers/dom/client/utils/setTextContent.js: DOM operation, setting text
renderers/dom/client/utils/DOMLazyTree.js: DOM operation, appending child
renderers/shared/stack/reconciler/ReactMultiChild.js: the middle method to traverseAllChildren
shared/utils/traverseAllChildren.js: the loop for instantiating the ReactDOMComponents
from direct sub-ReactElement
s
Notations used in the call stack:
↻ loop
? condition
I use {} to reference the previous post that is relevant to the methods (or logic process) being discussed.
The process discussed in this post occurs majorly within ReactDOMComponent[6].mountComponent()
. The major task of this method, that derives a DOM object from ReactDOMComponent[6]
, is covered in {post three}.
This task is numbered as 0) for later reference.
In this post, we are going to address one of the methods that we overlooked on purpose last time, _createInitialChildren
which is used for handling the newly introduced ReactElement
tree as the class component’s children.
It was used in a niche case, a text child, in {post three *7} and only a side branch was triggered. This side branch, as well as the entire method will
be discussed in detail in this post.
_createInitialChildren
is our protagonist today; please search *7 in post three if you want to check its role in the simple component rendering. The other overlooked method_updateDOMProperties
in {post three *6} will be discussed in later articles.
To be more specific, this method 1) transforms ReactElement
s to their corresponding ReactDOMComonent
s; 2) (recursively) calls ReactDOMComponent[*].mountComponent()
to create the DOM objects; and 3) appends them
to the root DOM node that is created in 0).
So firstly let’s recap step 0) in the context of class component.
ReactDOMComponent[6].mountComponent()
(before _createInitialChildren
)—create the DOM element[6]
time-saving hint: this paragraph is here to keep the post self-contained, the detail of the
ReactDOMComponent
creation process has been covered in {post three}
Designated data structure:
Call stack in action:
...
|~mountComponentIntoNode() |
|-ReactReconciler.mountComponent() |
|-ReactCompositeComponent[T].mountComponent() |
|-ReactCompositeComponent[T].performInitialMount() upper half
|-ReactReconciler.mountComponent() |
|-ReactCompositeComponent[ins].mountComponent() |
|-this.performInitialMount() |
|-this._renderValidatedComponent() |
|-instantiateReactComponent() _|_
(we are here) |
|-ReactDOMComponent[6].mountComponent( |
transaction, // scr: -----> not of interest |
hostParent, // scr: -----> null |
hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins] lower half
context // scr: -----> not of interest |
) |
...
This step creates a DOM object with ReactDOMComponent[6]
, and set up its attributes.
To recap: it 1) initializes properties of ReactDOMComponent[6]
; 2) creates a div
DOM element using document.createElement(); 3) creates
a double link between ReactDOMComponent[6]
and the DOM object; 4) & 5) set the properties and attributes of the newly created DOM object; and 6) embeds the DOM object in DOMLazyTree[1]
.
mountComponent: function (
transaction,
hostParent,
hostContainerInfo,
context
) {
// scr: --------------------------------------------------------> 1)
this._rootNodeID = globalIdCounter++;
this._domID = hostContainerInfo._idCounter++;
this._hostParent = hostParent;
this._hostContainerInfo = hostContainerInfo; // scr: ------------> ReactDOMContainerInfo[ins]
var props = this._currentElement.props;
switch (this._tag) { // scr: ---> no condition is met here
...
}
... // scr: -----> sanity check
// We create tags in the namespace of their parent container, except HTML
// tags get no namespace.
var namespaceURI;
var parentTag;
if (hostParent != null) { // scr: -----> it is null
...
} else if (hostContainerInfo._tag) {
namespaceURI = hostContainerInfo._namespaceURI; // scr: -------> "http://www.w3.org/1999/xhtml"
parentTag = hostContainerInfo._tag; // scr: ------> "div"
}
if (namespaceURI == null ||
namespaceURI === DOMNamespaces.svg &&
parentTag === 'foreignobject'
) { // scr: -----> no
...
}
if (namespaceURI === DOMNamespaces.html) {
if (this._tag === 'svg') { // scr: -----> no
...
} else if (this._tag === 'math') { // scr: -----> no
...
}
}
this._namespaceURI = namespaceURI; // scr: ---------------------> "http://www.w3.org/1999/xhtml"
... // scr: ------> DEV code
var mountImage;
if (transaction.useCreateElement) { // scr: ---------------------> transaction related logic, we assume it is true
var ownerDocument = hostContainerInfo._ownerDocument;
var el;
if (namespaceURI === DOMNamespaces.html) {
if (this._tag === 'script') { // scr: -----> no
...
} else if (props.is) { // scr: -----> no
...
} else {
// Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
// scr: --------------------------------------------------------> 2)
// scr: ---------> HTML DOM API
el = ownerDocument.createElement(this._currentElement.type);
}
} else { // scr: ------> no
...
}
// scr: --------------------------------------------------------> 3)
ReactDOMComponentTree.precacheNode(this, el); // scr: --------> doubly link (._hostNode & .internalInstanceKey)
this._flags |= Flags.hasCachedChildNodes; // scr: ------------>
bit wise its flags
// scr: --------------------------------------------------------> 4)
if (!this._hostParent) { // scr: ------> it is the root element
DOMPropertyOperations.setAttributeForRoot(el); // scr: -----> data-reactroot
}
// scr: --------------------------------------------------------> 5)
this._updateDOMProperties( //*6
null,
props,
transaction
); // scr: --------------------------> style:{ “color”: “blue” }
// scr: --------------------------------------------------------> 6)
var lazyTree = DOMLazyTree(el); // scr: ------> DOMLazyTree[ins]
this._createInitialChildren(transaction, props, context, lazyTree);
...
} // if (transaction.useCreateElement)
return mountImage;
}
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
ReactDOMComponent[6]
gets its DOM node, its children are next in the line
ReactDOMComponent[6]._createInitialChildren()
—create DOM elements[2-5]
Designated data structure:
As mentioned before, this method was used to create the string child node (‘hello world’
) in {post three}. We will reuse this side branch in this
post when rendering similar node, and we name it as route {1}
In the case of class component, route {2} is hit when this method is accessed the first time. To be specific, this branch handles the ReactElement
tree. As mentioned, it 1) transforms ReactElement
s to ReactDOMComponents
(a), generates DOM nodes with ReactDOMComponents
(b), and 2) insert DOM nodes to the root node generated with ReactDOMComponent[6]
in last step.
_createInitialChildren: function (
transaction, // scr: not of interest
props, // scr: -------------------> ReactElement[6].props
context, // scr: not of interest
lazyTree // scr: -------------------> DOMLazyTree[ins]
) {
// Intentional use of != to avoid catching zero/false.
// scr: it is named as 'dangerous', let's avoid touching it
var innerHTML = props.dangerouslySetInnerHTML;
if (innerHTML != null) { // scr: so no innerHTML
...
} else {
var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
var childrenToUse = contentToUse != null ? null : props.children;
// scr: some comments
if (contentToUse != null) {
// scr: some comments
if (contentToUse !== '') { // scr: ----------------> route {1}
...// scr: DEV code
DOMLazyTree.queueText(lazyTree, contentToUse);
}
} else if (childrenToUse != null) { // scr: ---------> route {2}
var mountImages = this.mountChildren(childrenToUse, transaction, context); // scr: --------------------------------> 1)
for (var i = 0; i < mountImages.length; i++) { scr: ------> 2)
DOMLazyTree.queueChild(lazyTree, mountImages[i]);
}
}
}
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
The call hierarchy and iteration is a bit complex from now on, so this time I’ll first establish an overview of the big picture before diving into any detail.
The static call stack:
... (outer recursion)
ReactDOMComponent[6].mountComponent() <-------------------------|
(we are here) |
|-this._createInitialChildren() |
?{1} |
|-DOMLazyTree.queueText() |
?{2} |
|-this.mountChildren() // scr: ---------------> 1)(a) |
|-this._reconcilerInstantiateChildren() |
|-ReactChildReconciler.instantiateChildren() |
|-traverseAllChildren() |
|-traverseAllChildrenImpl() <------|inner |
|↻traverseAllChildrenImpl() ------|recursion |
|-instantiateChild() |
|-instantiateReactComponent() |
|↻ReactDOMComponent.mountComponent() // scr: -> 1)(b)---|
|↻DOMLazyTree.queueChild() // scr: ---------------> 2)
...
First we examine the bottom of the (complex) stack that operates on the DOM level (for releif).
DOMLazyTree.queueText()
and DOMLazyTree.queueChild()
DOMLazyTree.queueText()
has only one effective line in this walk-through:
function queueText(tree, text) {
if (enableLazy) { // scr: NO, I mean, false
...
} else {
setTextContent(tree.node, text);
}
}
queueText@renderers/dom/client/utils/DOMLazyTree.js
var setTextContent = function (node, text) {
if (text) {
var firstChild = node.firstChild;
if (firstChild && firstChild === node.lastChild && firstChild.nodeType === 3) { // scr: false
...
}
}
node.textContent = text; // scr: the only effective line
};
setTextContent@renderers/dom/client/utils/setTextContent.js
Node.textContent is a DOM standard property for, well, text content of a node.
DOMLazyTree.queueChild()
has one line as well:
function queueChild(parentTree, childTree) {
if (enableLazy) { // scr: again, false
...
} else {
parentTree.node.appendChild(childTree.node);
}
}
queueChild@renderers/dom/client/utils/DOMLazyTree.js
Here Node.appendChild() is yet another DOM standard API.
Now we can replace these two methods with their respective essence line.
... (outer recursion)
ReactDOMComponent[6].mountComponent() <-------------------------|
|-this._createInitialChildren() |
?{1} |
|-node.textContent = text; |
?{2} |
|-this.mountChildren() // scr: ---------------> 1)(a) |
|-this._reconcilerInstantiateChildren() |
|-ReactChildReconciler.instantiateChildren() |
|-traverseAllChildren() |
|-traverseAllChildrenImpl() <------|inner |
|↻traverseAllChildrenImpl() ------|recursion |
|-instantiateChild() |
|-instantiateReactComponent() |
|↻ReactDOMComponent.mountComponent() // scr: ------> 1)(b)---|
|↻node.appendChild() // scr: ------> 2)
...
Extrapolate the big picture
To do so, we start from methods that we already know.
First, instantiateReactComponent()
which instantiates a ReactDOMComponent
from ReactElement
(we do not have any “composite” ReactElement
in the tree now so all the components being created are ReactDOMComponent
s),
which is also the end of the deeply nested call hierarchy {post two}
Second, ReactDOMComponent.mountComponent()
which initializes the ReactDOMComponents
created in the previous step and create the corresponding DOM nodes based on them. {post three}
& {beginning}
Considering the above two operations an {OG} (operation group), it is now easier to explain how the rest of the ReactElement
tree is processed.
Here is a high-end explanation:
-
when outer recursion of
ReactDOMComponent.mountComponent()
is called for non-leaf node, branch {2} will be taken to trigger {OG} for each of the component’s children; -
when outer recursion of
ReactDOMComponent.mountComponent()
is called for leaf node that contains text, branch {1} will be in action, which setnode.textContent
; -
when outer recursion of
ReactDOMComponent.mountComponent()
is called for leaf node that does not contain text,_createInitialChildren()
will not be called at all.
Please note that in the discussion above, ReactDOMComponent.mountComponent()
is also used to create DOM node for the respective ReactDOMComponent
instance, so you might need to check it in the begining of this text if it
is not in your (brain) cache.
It’s time to draw the call stack in action:
...
ReactDOMComponent[6].mountComponent()
|-this._createInitialChildren()
|-this.mountChildren()
... |↻instantiateReactComponent()[4,5]
|-ReactDOMComponent[5].mountComponent()
|-this._createInitialChildren()
|-node.textContent = text; // scr: [5] done
|-ReactDOMComponent[4].mountComponent()
|-this._createInitialChildren()
|-this.mountChildren()
... |↻instantiateReactComponent()[2,3]
|-ReactDOMComponent[2].mountComponent() // scr: [2] done
|-ReactDOMComponent[3].mountComponent()
|-this._createInitialChildren()
|-node.textContent = text; // scr: [3] done
|↻node[4].appendChild()[2,3] // scr: [4] done
|↻node[6].appendChild()[4,5] // scr: [6] done
...
In this call stack I omit the deep nested call hierarchy that is used to instantiate the ReactDOMComponent
from ReactElement
as we are going to examine it next. In particular, we need to pay attention to the arguments to
keep track the input & output across the fairly deep and complex chain that enlists recursion and callback.
The deep nested loop of instantiateReactComponent()
Starting from inside ReactDOMComponent._createInitialChildren()
:
...
var mountImages = this.mountChildren(
childrenToUse, // scr:----------> ReactElement[6].props.children
transaction, // scr: not of interest
context // scr: not of interest
);
...
Next we look at the implementation of ReactDOMComponent.mountChildren()
. As mentioned before, it 1) instantiates all the children of ReactDOMComponents
; and 2) initializes those ReactDOMComponent
by calling ReactDOMComponent.mountComponent()
.
mountChildren: function (
nestedChildren, // scr:----------> ReactElement[6].props.children
transaction, // scr: not of interest
context // scr: not of interest
) {
// scr: ------------------------------------------------------> 1)
var children = this._reconcilerInstantiateChildren(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 selfDebugID = 0;
...// scr: DEV code
// scr: --------------------------------------------------> 2)
var mountImage = ReactReconciler.mountComponent(child, transaction, this, this._hostContainerInfo, context, selfDebugID);
child._mountIndex = index++;
mountImages.push(mountImage);
}
}
...// scr: DEV code
return mountImages;
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js
2) was referred to as “outer recursion” before and is yet another ReactReconciler.mountComponent()
{post two}, so we focus on 1)
_reconcilerInstantiateChildren: function (
nestedChildren, // scr:----------> ReactElement[6].props.children
transaction, // scr: not of interest
context // scr: not of interest
) {
...// scr: DEV code
return ReactChildReconciler.instantiateChildren(nestedChildren, transaction, context);
},
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
which is a direct call of
instantiateChildren: function (
nestedChildNodes, // scr: --------> ReactElement[6].props.children
transaction, // scr: not of interest
context, // scr: not of interest
selfDebugID
) // 0 in production and for roots {
if (nestedChildNodes == null) {
return null;
}
var childInstances = {};
if (process.env.NODE_ENV !== 'production') {
...// scr: DEV code
} else {
traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
}
return childInstances;
},
instantiateChildren@renderers/shared/stack/reconciler/ReactChildReconciler.js
which is, again, a direct call to traverseAllChildren()
. Note that instantiateChild
is the callback method which is invoked for each child.
function instantiateChild(
childInstances, // scr: ---> the output parameter childInstances is passed all the way down here
child, // scr: --> a ReactElement
name, // scr: --> unique name for indexing in childInstances
selfDebugID // scr: --> undefined
) {
... // scr: DEV code
}
if (child != null && keyUnique) {
childInstances[name] = instantiateReactComponent(child, true);
}
}
instantiateChild@renderers/shared/stack/reconciler/ReactChildReconciler.js
It in turn calls instantiateReactComponent()
{post one} directly.
We move on to traverseAllChildren()
function traverseAllChildren(
children, // scr: ---------> ReactElement[6].props.children
callback, // scr: ---------> instantiateChild
traverseContext // scr: ---> output parameter, initialized as {}
) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
traverseAllChildren@shared/utils/traverseAllChildren.js
yet another direct call to traverseAllChildrenImpl()
function traverseAllChildrenImpl(
children, // scr: ---------> ReactElement[6].props.children
nameSoFar, // scr: ---------> ''
callback, // scr: ---------> instantiateChild
traverseContext // scr: ---> output parameter, initialized as {}
) {
var type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
// scr: -------------------------------------------------------> {a}
if (children === null || type === 'string' || type === 'number' || type === 'object' && children.?typeof === REACT_ELEMENT_TYPE) {
callback(traverseContext, children,
// If it's the only child, treat the name as if it was wrapped in an array
// so that it's consistent if the number of children grows.
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
return 1;
}
var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
// scr: -------------------------------------------------------> {b}
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
}
} else {
... // scr: this branch will not be called here
}
return subtreeCount;
}
traverseAllChildrenImpl@shared/utils/traverseAllChildren.js
After those directly one-line calls of another method, traverseAllChildrenImpl()
is the true workhouse. this method was also referred to as “inner recursion” soon before.
The logic of traverseAllChildrenImpl()
is quite straight forward: when it is called the first time (the type of children
is array), it call itself for every ReactElement
in this array; when it is call for ReactElement
,
it invokes the aforementioned callback that uses instantiateReactComponent()
internally {post one} to transform a ReactElement
to
an empty and uninitialized ReactDOMComonent
.
Note that the “inner recursion” is only conducted for DIRECT children while the “outer recursion” traverse the entire
ReactElement
tree.
After all the ReactElements
are transformed to ReactDOMComonent
s, the output is returned all the way back to ReactDOMComponent.mountChildren()
and complete the circle.
To better understand the full circle, you might need to refer to different pieces of the puzzle back and forth, for example, the beginning of this text where
ReactDOMComponent.mountComponent()
is discussed; the two DOM operations (Node.appendChild, Node.textContent) that define the stack bottom; the discussion of the big picture; as well as this section.
At last, after all the DOM nodes are generated, the logic returns back to ReactReconciler.mountComponent()
and the new node is inserted to the designated container. {post three}
...
|~mountComponentIntoNode() |
|-ReactReconciler.mountComponent() |
|-ReactCompositeComponent[T].mountComponent() |
|-ReactCompositeComponent[T].performInitialMount() upper half
|-ReactReconciler.mountComponent() |
|-ReactCompositeComponent[ins].mountComponent() |
|-this.performInitialMount() |
|-this._renderValidatedComponent() |
|-instantiateReactComponent() _|_
|-ReactDOMComponent[6].mountComponent( |
transaction, // scr: -----> not of interest |
hostParent, // scr: -----> null |
hostContainerInfo,// scr:---------------------> ReactDOMContainerInfo[ins] |
context // scr: -----> not of interest |
) |
|
... // the content of this section lower half
|-_mountImageIntoNode() (HTML DOM specific)
markup, // scr: --> DOMLazyTree[ins] |
container, // scr: --> document.getElementById(‘root’)
wrapperInstance, // scr:----> same |
shouldReuseMarkup, // scr:--> same |
transaction, // scr: -------> same |
) _|_