Last time we went through the process from setState() to the updating of a single DOM. We also analyzed the diffing algorithm, which is far from complete as the algorithm is designed for tasks that are much more complex than updating a single DOM node.
This time we are going to use two examples to examine the diffing algorithm more in depth. More specific, we look at how the algorithm deals with a mutating DOM tree.
N.b., the examples used in this article are derived from the official document which also provides a high level description of the diffing algorithm. You might want to read it first if the topic does not seem very familiar.
We know the result virtual DOM tree of the render() method is {post four} (nested calling of React.createElement())
We ignore the ReactElement’s corresponding controllers (i.e., ReactDOMComponent) for simplicity.
The figure above gives the old virtual DOM tree that is generated by the initial rendering. As in {last post}, a setState() is fired after 5 seconds, which initiates the updating process,
Figure-I
With this data structure in mind, we skip the logic process (mostly, before transaction) that is identical to {last post}, and move directly to the diffing algorithm,
in {last post} step 1) updates a DOM node properties; and 2) updates its content.
But for the the root node (ReactElement[1]) the only purpose of the whole ReactDOMComponent.updateComponent() method call is to recurse and update ReactElement[1]’s direct children because neither the node’s properties and its content are changed.
I also extend the static call stack from {last post} as a lead:
As mentioned before, the recursing starts from ReactDOMComponent._updateDOMChildren(). In the following sections, we will follow the hierarchy, one function a time, and go for the bottom of the stack.
ReactDOMComponent._updateDOMChildren() — Start recursing direct children
I fold up the content updating related code so we can focus on DOM children recursing
1) remove the children only when necessary (lastChildren != null && nextChildren == null);
2) start the recursing.
ReactMultiChild.updateChildren() I — The actual work horse
After the methods that are either alias or those with very little (preprocessing) operations, we come to the work horse that I) recurses virtual DOM children, compares the new/old versions of them and modifies ReactDOMComponent’s accordingly (we name them virtual DOM operations for simplicity); and II) commits the operations to real DOMs.
the role of this ReactMultiChild.updateChildren() is similar to that of mountComponentIntoNode() in initial rendering {post two}
updateChildren: function ( nextNestedChildrenElements, transaction, context ) { // Hook used by React ART this._updateChildren(nextNestedChildrenElements, transaction, context); },
_updateChildren: function ( nextNestedChildrenElements, transaction, context ) { var prevChildren = this._renderedChildren; var removedNodes = {}; var mountImages = []; var nextChildren = this._reconcilerUpdateChildren( // scr: ---> I) prevChildren, // scr: ------------------> i) nextNestedChildrenElements, // scr: ----> ii) mountImages, removedNodes, transaction, context );
if (!nextChildren && !prevChildren) { return; }
// scr: -----------------------------------------------------> II) var updates = null; var name; // `nextIndex` will increment for each child in `nextChildren`, but // `lastIndex` will be the last index visited in `prevChildren`. var nextIndex = 0; var lastIndex = 0; // `nextMountIndex` will increment for each newly mounted child. var nextMountIndex = 0; var lastPlacedNode = null; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; }
var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. }
// The child must be instantiated before it's mounted. updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context)); nextMountIndex++; }
// Remove children that are no longer present. for (name in removedNodes) { if (removedNodes.hasOwnProperty(name)) { updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name])); } }
if (updates) { processQueue(this, updates); } this._renderedChildren = nextChildren;
We firstly look at the virtual DOM operations, I). Note that the two input parameters of the responsible method ReactDOMComponent._reconcilerUpdateChildren() are i) prevChildren, i.e., ReactDOMComponent._renderedChildren which is set to an object of its sub-ReactDOMComponents in initial rendering {post five}; and ii) nextNestedChildrenElements, i.e., nextProps.children passed from ReactDOMComponent._updateDOMChildren().
ReactDOMComponent._reconcilerUpdateChildren() — Virtual DOM operations
functionflattenSingleChildIntoContext( traverseContext, child, name, selfDebugID ) { // We found a component instance. if (traverseContext && typeof traverseContext === 'object') { var result = traverseContext; var keyUnique = result[name] === undefined;
, which set individual ReactElement with its associated key (name) in an object (map). Next we look at the traverseAllChildren() method body, to see in particular how the keys are generated.
functiontraverseAllChildrenImpl( children, nameSoFar, // scr: -------- '' callback, traverseContext ) { var type = typeof children;
if (type === 'undefined' || type === 'boolean') { // All of the above are perceived as null. children = null; }
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); return1; }
var child; var nextName; var subtreeCount = 0; // Count of children found in the current subtree. var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
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: code that is not applicable ... }
when it is called the first time (and the type of children parameter is array), it calls itself for every ReactElement within the array; when it is called successively (children is ReactElement), invokes the aforementioned callback that…
“set individual ReactElement with its associated key (name) in an object” as mentioned soon before.
The keys are generated with getComponentKey(),
1 2 3 4 5 6 7 8 9 10
functiongetComponentKey(component, index) { if (component && typeof component === 'object' && component.key != null) { // Explicit key return KeyEscapeUtils.escape(component.key); } // Implicit key determined by the index in the set return index.toString(36); }
which basically uses the index of the array as the key in the object (index.toString(36)), in the case that the key is not explicitly set in “key-less nodes”.
The static (sub) call stack of flattenChildren(),
1 2 3 4 5 6
... flattenChildren() |-traverseAllChildren() |-traverseAllChildrenImpl() |↻traverseAllChildrenImpl() // for direct each child |-flattenSingleChildIntoContext()
now we have an key-value object nextChildren to be “diffed” with prevChildren.
ReactChildReconciler.updateChildren() — manipulate the virtual DOM tree
// The child must be instantiated before it's mounted. var nextChildInstance = instantiateReactComponent(nextElement, true); nextChildren[name] = nextChildInstance; // Creating mount image now ensures refs are resolved in right order // (see https://github.com/facebook/react/pull/7101 for explanation). var nextChildMountImage = ReactReconciler.mountComponent( nextChildInstance, transaction, hostParent, hostContainerInfo, context, selfDebugID, );
mountImages.push(nextChildMountImage); } // scr: ----------------------------------------------> end 2) }
// scr: ------------------------------------------------------> 3) // Unmount children that are no longer present. for (name in prevChildren) { if ( prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name)) ) { prevChild = prevChildren[name]; removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } } // scr: ------------------------------------------------> end 3) },
updating is nothing more than modifying, adding, and deleting
This method traverse the nextChildren, and
1) recurse back to ReactReconciler.receiveComponent() to modify the content of the associated DOM nodes as in {last post} if the types of the corresponding “pre” and “next” nodes are the same (judged by shouldUpdateReactComponent() {last post}), the logic branch of which applies to
and
as the comparison is based on the counterparts’ index (that is also key);
2) re-mount the virtual DOM if types of “pre” and “next” nodes are different, or the corresponding “pre” node simply does not exist;
As in {post five}, the virtual DOM’s corresponding li node has been created in the mounting process;
3) un-mount “pre” virtual DOM(s) if they do not exist in the “next” ones.
The content updating operations are encapsulated in the recursion of ReactReconciler.receiveComponent() {last post}, whilst the operations on real DOM tree are conducted when the logic processes back in ReactMultiChild.updateChildren().
ReactMultiChild.updateChildren() II — matipulate real DOMs
... var updates = null; var name; // `nextIndex` will increment for each child in `nextChildren`, but // `lastIndex` will be the last index visited in `prevChildren`. var nextIndex = 0; var lastIndex = 0; // `nextMountIndex` will increment for each newly mounted child. var nextMountIndex = 0; var lastPlacedNode = null; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; }
// scr: --------------------------------------------------> III) var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { updates = enqueue( updates, this.moveChild( prevChild, lastPlacedNode, nextIndex, lastIndex ) );
lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; // scr: ---------> end III) } else { // scr: ------------------------------------------> IV) if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. }
// The child must be instantiated before it's mounted. updates = enqueue( updates, this._mountChildAtIndex( nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context ) );
nextMountIndex++; } // scr: ---------------------------------------------> end IV)
// Remove children that are no longer present. for (name in removedNodes) { // scr: -------------------------> V) if (removedNodes.hasOwnProperty(name)) { updates = enqueue( updates, this._unmountChild( prevChildren[name], removedNodes[name] ) ); } } // scr: ------------------------------------------------> end V)
if (updates) { processQueue(this, updates); // scr: ----------------------> VI) }
processUpdates: function(parentNode, updates) { // scr: DEV code ...
for (var k = 0; k < updates.length; k++) { var update = updates[k]; switch (update.type) { case'INSERT_MARKUP': insertLazyTreeChildAt( parentNode, update.content, getNodeAfter(parentNode, update.afterNode), ); break; // scr: code that is not applicable ...
The process logic are the same as in key-less nodes before ReactDOMComponent.flattenChildren(), in which the designated keys instead of the array index will be used to establish the key-value object,
1 2 3 4 5 6 7 8 9 10 11
functiongetComponentKey(component, index) { if (component && typeof component === 'object' && component.key != null) { // Explicit key return KeyEscapeUtils.escape(component.key); } // code that is not applicable ... }
So in ReactChildReconciler.updateChildren() the comparison of the two virtual DOM trees can be better aligned,
and the recursive ReactReconciler.receiveComponent() does not incur any DOM operations by comparing nodes (key: one and two) with same content , and only the necessary DOM operation, i.e.,
The above code also changes the DOM tree structure. Can you answer why the keys are not required here?
—End note—
Reading source code with a purpose is like searching an array, in which, theoretically, it is O(n) - O(log n) faster when the array has already been sorted. This series aims to sort out the React code base for you, so you will be able to enjoy the O(log n) whenever having a purpose(s).
That's it. Did I make a serious mistake? or miss out on anything important? Or you simply like the read. Link me on -- I'd be chuffed to hear your feedback.