Last time we went from setState() to a single DOM update and briefly analyzed the Diffing algorithm. This analysis is obviously insufficient, since the Diffing algorithm was designed from the outset to deal with more complex cases.

In this article we will take a closer look at the Diffing algorithm with two examples. More specifically, let’s look at how the algorithm handles structural changes in the DOM tree.

Note that the examples used in this article are drawn from official documentation. This document also provides a comparative description of the Diffing algorithm. So if you are not familiar with the topic of this article, you may want to read it first.

Photo by Luke Leung on Unsplash

Example 1: Diff a keyless node

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data : ['one', 'two'],
    };

    this.timer = setInterval(
      () => this.tick(),
      5000
    );
  }

  tick() {
    this.setState({
      data: ['new', 'one', 'two'],
    });
  }

  render() {
    return (
      <ul>
      {
        this.state.data.map(function(val, i) {
          return <li>{ val }</li>;
        })
      }
      </ul>
    );
  }
}

export default App;
Copy the code

A babeled version:

render() { return React.createElement( 'ul', null, this.state.data.map(function (val, i) { return React.createElement( 'li', null, ' ', val, ' ' ); })); }Copy the code

New and old virtual DOM trees

We already know that the render() function generates the following virtual DOM tree (via nested calls to react.createElement ()).


We ignore the ReactDOMComponent for each ReactElement

The figure above shows the old virtual DOM tree generated during the initial rendering process. As in {previous}, setState() fires after 5 seconds and starts the interface update process,

Figure-I

To give an impression of this data structure, we skip the logic process that is repeated with {previous} (mostly before transaction begins) and go directly to the Diffing algorithm.

_updateRenderedComponent: function (transaction, context) {
  var prevComponentInstance = this._renderedComponent; // scr: -> 1)

  // scr: ------------------------------------------------------> 2)
  var prevRenderedElement = prevComponentInstance._currentElement;

  // scr: create a new DOM tree
  var nextRenderedElement = this._renderValidatedComponent();

  var debugID = 0;

  // scr: DEV code
...

  if (shouldUpdateReactComponent( // scr: ----------------------> 3)
      prevRenderedElement,
      nextRenderedElement)
  ) {
    ReactReconciler.receiveComponent( // scr: ------------------> 5)
      prevComponentInstance,
      nextRenderedElement,
      transaction,
      this._processChildContext(context)
    );
  } else { // scr: ---------------------------------------------> 4)
  // scr: code that is not applicable this time
...
  }
},

ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js
Copy the code

Steps 1-5) and {
In the previous}, too, will not be repeated here

As discussed in {4}, the algorithm starts with a new DOM tree (the one on the right of Figure-I) with the ReactCompositecomponent._renderValidatedComponent () component.

As with nodes, diff child nodes

Since the old and new ReactElement[1] are of the same type (” ul “), the logic goes to 5 as in {previous}).

receiveComponent: function (nextElement, transaction, context) { var prevElement = this._currentElement; this._currentElement = nextElement; this.updateComponent(transaction, prevElement, nextElement, context); }, updateComponent: function( transaction, prevElement, nextElement, context ) { var lastProps = prevElement.props; var nextProps = this._currentElement.props; // scr: code that is not applicable this time ... // scr: ------------------------------------------------------> 1) this._updateDOMProperties(lastProps, nextProps, transaction);  // scr: ------------------------------------------------------> 2) this._updateDOMChildren(lastProps, nextProps, transaction, context); // scr: code that is not applicable this time ... }, ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.jsCopy the code

In {previous}, step 1 updates the attributes of the DOM node; And 2) update its content.

But for the and node (ReactElement [1]), because of the old and new version, so the whole ReactDOMComponent. UpdateComponent () call only do one thing, traverse and update its direct child nodes.

Let me extend the static call stack from {last} to introduce the following discussion:

. ___ ReactReconciler.receiveComponent() <----------------| | |-ReactDOMComponent.receiveComponent() | | |-this.updateComponent() | | |-this._updateDOMProperties() | | |-CSSPropertyOperations.setValueForStyles() | | |-this._updateDOMChildren() | | |-this.updateTextContent() | diffing |-this._updateDOMChildren() (the focus this time)| | |-this.updateChildren() | | |=this._updateChildren() | | |-this._reconcilerUpdateChildren() | | |-this.flattenChildren() | | |-ReactChildReconciler.updateChildren() ---| | ---Copy the code

As mentioned earlier, the traversal (child nodes) starts with the reactdomComponent._updatedomChildren () method. In the following sections, we will go to the bottom of the stack one function at a time.

ReactDOMComponent._updateDOMChildren()— Start Recursing Direct children

_updateDOMChildren: function ( lastProps, nextProps, transaction, context ) { // scr: code for content updating ... var nextChildren = nextContent ! = null ? null : nextProps.children; if (lastChildren ! = null && nextChildren == null) { // scr: --> 1) this.updateChildren(null, transaction, context); } else if (lastHasContentOrHtml && ! nextHasContentOrHtml) { // scr: code for content updating ... } if (nextContent ! = null) { if (lastContent ! == nextContent) { // scr: code for content updating ... } else if (nextHtml ! = null) { // scr: code for content updating ... } else if (nextChildren ! = null) { // scr: DEV code ... // scr: --------------------------------------------------> 2) this.updateChildren(nextChildren, transaction, context); } }, ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.jsCopy the code

I’ve collapsed the code for content updates to focus on sub-DOM traversal

1) If the conditions are met (lastChildren! NextChildren == null &&nextChildren == null);

2) Start traversal.

ReactMultiChild.updateChildren()I — real work

After a few aliases (and a few other variously sketched functions), this is the first function that actually does anything. I) It iterates through the virtual DOM child nodes, compares the old and new versions and updates the ReactDOMComponent (we call this the virtual DOM operation for short); II) Solidify recorded operations into the real DOM.

Here ReactMultiChild. UpdateChildren () is similar to the role of first render mountComponentIntoNode () the second {}

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++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // 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; // scr: DEV code ... ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.jsCopy the code

Let’s look at virtual DOM operations first, I). Note here the two arguments to the function reactdomComponent._ReconcilErupdatechildren (), I) prevChildren, ReactDOMComponents {5} are the child ReactDOMComponents assigned to ReactdomComponent. _renderedChildren in the first rendering; Ii) nextNestedChildrenElements is from ReactDOMComponent. _updateDOMChildren () coming nextProps. Children.

ReactDOMComponent._reconcilerUpdateChildren()– Virtual DOM operations

_reconcilerUpdateChildren: function (
  prevChildren,
  nextNestedChildrenElements,
  mountImages,
  removedNodes,
  transaction,
  context
) {
  var nextChildren;
  var selfDebugID = 0;
  // scr: DEV code
...

  nextChildren = flattenChildren(      // scr: -----------------> 1)
                   nextNestedChildrenElements,
                   selfDebugID);

  ReactChildReconciler.updateChildren( // scr: -----------------> 2)
                   prevChildren,
                   nextChildren,
                   mountImages,
                   removedNodes,
                   transaction,
                   this,
                   this._hostContainerInfo,
                   context, selfDebugID);

  return nextChildren;
},

ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
Copy the code

This method is called in step 2), before traversing and updating the child virtual DOM nodes

flattenChildren()ReactElementAn array becomes an object

function flattenChildren(children, selfDebugID) {
  if (children == null) {
    return children;
  }
  var result = {};

  // scr: DEV code
...

  {
    traverseAllChildren(children, flattenSingleChildIntoContext, result);
  }

  return result;
}

flattenChildren@shared/utils/flattenChildren.js
Copy the code

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

function flattenSingleChildIntoContext( traverseContext, child, name, selfDebugID ) { // We found a component instance. if (traverseContext && typeof traverseContext === 'object') { var result = traverseContext; var keyUnique = result[name] === undefined; // scr: DEV code ... if (keyUnique && child ! = null) { result[name] = child; } } } flattenSingleChildIntoContext@shared/utils/flattenChildren.jsCopy the code

, sets a single ReactElement and its corresponding key (name) to the target object. Let’s move on to the body of the traverseAllChildren() function to see how the key is generated.

. var SEPARATOR = '.'; . function traverseAllChildren(children, callback, traverseContext) { if (children == null) { return 0; } return traverseAllChildrenImpl(children, '', callback, traverseContext); } traverseAllChildren@shared/utils/traverseAllChildren.js function traverseAllChildrenImpl( 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); return 1; } 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 ... } return subtreeCount; } traverseAllChildrenImpl@shared/utils/traverseAllChildren.jsCopy the code

This is a function we discussed in Chapter 5, (let me copy it here)

When it is first called (this argument
childrenIs of type
array), it will evaluate all of the values in this array
ReactElementRecursively tune yourself again; When it is called later (argument
childrenis
ReactElement), which calls the previously mentioned callback function. This callback function is internally rearranged at……

“Set the single ReactElement and its corresponding key (name) to the target object” (above).

The key is generated by getComponentKey(),

function getComponentKey(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); } getComponentKey@shared/utils/traverseAllChildren.jsCopy the code

Use the index of the array as the key(index.toString(36)). It’s also because we’re talking about keyless nodes.

Current subcall stack,

. | | - traverseAllChildren flattenChildren () () - traverseAllChildrenImpl () | ↻ traverseAllChildrenImpl () / / for direct each child |-flattenSingleChildIntoContext()Copy the code

We now have an object with key-value pairs called nextChildren that can be used for “diff” prevChildren.

ReactChildReconciler.updateChildren()– Operates the virtual DOM tree

updateChildren: function( prevChildren, nextChildren, mountImages, removedNodes, transaction, hostParent, hostContainerInfo, context, selfDebugID, // 0 in production and for roots ) { if (! nextChildren && ! prevChildren) { return; } var name; var prevChild; for (name in nextChildren) { if (! nextChildren.hasOwnProperty(name)) { continue; } prevChild = prevChildren && prevChildren[name]; var prevElement = prevChild && prevChild._currentElement; var nextElement = nextChildren[name]; if ( // scr: -----------------------------------------------> 1) prevChild ! = null && shouldUpdateReactComponent(prevElement, nextElement) ) { ReactReconciler.receiveComponent( prevChild, nextElement, transaction, context, ); nextChildren[name] = prevChild; // scr: --------------> end 1) } else { if (prevChild) { // scr: ---------------------------------> 2) removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild, false); } // 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) },Copy the code

The so-called “update” is nothing more than add, delete, change

This function will iterate over nextChildren, and then

1) if the “pre” and “next” of the same type (by shouldUpdateReactComponent () to determine), traversing back to the ReactReconciler. ReceiveComponent () and update the child nodes on {a} content, in particular, the branch will be updated


As well as


2) If the types of “pre” and “Next” are different, or the corresponding “pre” does not exist at all, remount the virtual DOM node;

In Chapter 5, the Li node corresponding to the virtual DOM was created during the mounting process


3) If “Pre” does not exist in “Next”, unmount the “pre” virtual DOM.

Update node content operations are encapsulated in the traversal ReactReconciler. ReceiveComponent on {a} in the (), but for the actual operation of the DOM tree requires the processing logic, such as return to 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) nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // 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) } this._renderedChildren = nextChildren; // scr: DEV code ... ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.jsCopy the code

This logical block will iterate over nextChildren, and then depending on the situation

III) Changes in the position of marked nodes;

IV) Mark new nodes;

V) Mark to delete nodes;

VI) solidify updates into real DOM trees.

The branch in effect here is IV), which adds the node corresponding to ReactElement[4] to the DOM tree.

_mountChildAtIndex: function (
  child,
  mountImage,
  afterNode,
  index,
  transaction,
  context
) {
  child._mountIndex = index;
  return this.createChild(child, afterNode, mountImage);
},

createChild: function (child, afterNode, mountImage) {
  return makeInsertMarkup(mountImage, afterNode, child._mountIndex);
},

function makeInsertMarkup(markup, afterNode, toIndex) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'INSERT_MARKUP',
    content: markup,
    fromIndex: null,
    fromNode: null,
    toIndex: toIndex,
    afterNode: afterNode
  };
}

ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js
Copy the code

And then 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
...

function insertLazyTreeChildAt(
  parentNode,
  childTree,
  referenceNode
) {
  DOMLazyTree.insertTreeBefore(
    parentNode,
    childTree,
    referenceNode
  );
}

DOMChildrenOperations@renderers/dom/client/utils/DOMChildrenOperations.js
Copy the code

So this number is DOMLazyTree insertTreeBefore (). From {third}, we know that this function calls the HTML DOM API

parentNode.insertBefore(tree.node, referenceNode); 
Copy the code

What’s the difference between a key node?

Diffing contains key nodes

Example 2,

. render() { return ( <ul> { this.state.data.map(function(val, i) { return <li key={val}>{ val }</li>; }) } </ul> ); }...Copy the code

Before the whole logic in ReactDOMComponent. FlattenChildren (), and without the same key nodes. In this function, which converts an array into an object containing key-value pair information, the keys contained in the node are displayed,

function getComponentKey(component, index) { if (component && typeof component === 'object' && component.key ! = null) { // Explicit key return KeyEscapeUtils.escape(component.key); } // code that is not applicable ... } getComponentKey@shared/utils/traverseAllChildren.jsCopy the code

So in the following comparison (ReactChildReconciler updateChildren ()), the old tree is, by its key and


Thus ReactReconciler. ReceiveComponent () in the recursive call to (key: one and two) to generate any actual DOM manipulation, because their contents are the same. So the only DOM manipulation required this time is

parentNode.insertBefore(tree.node, referenceNode);
Copy the code

Used to add a node with key new to the DOM tree.

Twice less than before.

Pack out

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      mutate: false,
    };

    this.timer = setInterval(
      () => this.tick(),
      5000
    );
  }

  tick() {
    this.setState({
      mutate: true,
    });
  }

  render() {
    return (
      <ul>
        { this.state.mutate &&
        <li>New</li>
        }
        <li>One</li>
        <li>Two</li>
      </ul>
    );
  }
}

export default App;
Copy the code

The code above also changes the structure of the DOM tree, so why not add keys to the nodes? The answer is in the code.


Conclusion (Yes, that’s all for now)

Read code purposefully is like a look-up algorithm, if the array is sorted, it’s theoretically O(n) -O (log n) faster than an unordered array. This serial aims to straighten out the React code so that the next time you read it you can enjoy order log n speed.

All right, I’ll stop there this time. If you think this post is good, please like it or follow this column.

Thanks for reading! 👋