Understanding The React Source Code - UI Updating (DOM Tree) IX

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.

Example 1., diffing of key-less nodes

the babeled version of render(),

The old & new virtual DOM tree

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,

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,

The steps 1–5) are also identical to {last post}.

, which starts by creating the new DOM tree (the right one in {Figure-I}) with ReactCompositeComponent._renderValidatedComponent(). {post four}

Root nodes are the identical, so “diff” their direct children

Since the types of ReactElement[1] are the same (“ul”), the logic goes to 5) as in {last post}.

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}

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

Before 2) the virtual DOM can be traversed and compared, this method 1) calls

flattenChildren() — converts ReactElement array into an object (map)

Here we need to pay attention to the callback passed to traverseAllChildren()

, 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.

We have described this method in {post five},

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(),

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(),

now we have an key-value object nextChildren to be “diffed” with prevChildren.

ReactChildReconciler.updateChildren() — manipulate the virtual DOM tree

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

This logic block iterates the nextChildren, and when necessary, it

III) mark that a node’s position has changed;

IV) mark a newly added node;

V) mark a removed node;

VI) commit the changes to the DOM tree {last post}

The branch applies here is IV) that adds the ReactElement[4] associated node to the DOM tree.

And in VI)

So the last card in this stack is DOMLazyTree.insertTreeBefore(). We already know from {post three} that this method calls the HTML DOM API

So what happens when

Diffing nodes with keys

Example 2.

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,

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.,

is conducted for the node (key: new) in ReactMultiChild.updateChildren().

As a result, keys can spare some unnecessary DOM operations for mutating a DOM tree .

Take home

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.