Basic Practice of javascript (11) - Implementation of DOM-DIFF

Keywords: Javascript Attribute github

Catalog

Reference code will be uploaded to my github warehouse. Mutual powder is welcome: https://github.com/dashnowords/blogs/tree/master

I. Talking about Generating Real DOM from Virtual-Dom

In the last blog post Basic Practice of javascript (10) - Virtual DOM and Basic DFS Section 3 demonstrates how to use the tree structure of Virtual-DOM to generate real DOM. It was hoped that readers who are not familiar with depth-first traversal would first pay attention to and feel the basic process of traversal. So the DOM node used in the demonstration only contains the class name and text content. It has simple structure and can be used to directly stitch the string displayed in the console when the DOM structure is reproduced. Many reader comments indicate that there is still confusion about how to get real DOM nodes from Virtual-Dom.

So this section first adds rendering methods to Element classes, demonstrating how to convert Virtual-Dom to a real DOM node and render it on the page.

element.js sample code:

//Virtual-DOM Node Class Definition
class Element{
    /**
   * @param {String} tag 'div' Tag name
   * @param {Object} props { class: 'item' } Attribute set
   * @param {Array} children [ Element1, 'text'] Subelement set
   * @param {String} key option 
   */
  constructor(tag, props, children, key) {
     this.tag = tag;
     this.props = props;
     if (Array.isArray(children)) {
        this.children = children;
     } else if (typeof children === 'string'){
        this.children = null;
        this.key = children;
     }
     if (key) {this.key = key};
  }

  /**
   * Generating Real DOM from Virtual DOM
   * @return {[type]} [description]
   */
  render(){
     //Generate Tags
     let el = document.createElement(this.tag);
     let props = this.props;
     
     //Add attribute
     for(let attr of Object.keys(props)){
        el.setAttribute(attr, props[attr]);
     }

     //Processing child elements
     var children = this.children || [];

     children.forEach(function (child) {
         var childEl = (child instanceof Element)
         ? child.render()//If the child node is an element, it is constructed recursively
         : document.createTextNode(child);//Generate text nodes if it is text
         el.appendChild(childEl);
     });
      
     //Mount references to DOM nodes on objects for subsequent updates to DOM
     this.el = el;
     //Returns the generated real DOM node
     return el;
  }
}
//Provide a short factory function
function h(tag, props, children, key) {
    return new Element(tag, props, children, key);
}

Test the defined Element class:

var app = document.getElementById('anchor');
var tree = h('div',{class:'main', id:'body'},[
       h('div',{class:'sideBar'},[
          h('ul',{class:'sideBarContainer',cprop:1},[
               h('li',{class:'sideBarItem'},['page1']),
               h('li',{class:'sideBarItem'},['page2']),
               h('li',{class:'sideBarItem'},['page3']),
            ])
        ]),
       h('div',{class:'mainContent'},[
           h('div',{class:'header'},['header zone']),
           h('div',{class:'coreContent'},[
                 h('div',{fx:1},['flex1']),
                 h('div',{fx:2},['flex2'])
            ]),
           h('div',{class:'footer'},['footer zone']),
        ])
    ]);
//Generating offline DOM
var realDOM = tree.render();
//Mount DOM
app.appendChild(realDOM);

This time, instead of looking at the console, the content of virtual DOM has become rendered on the page by real DOM nodes.

Next, we formally enter the following steps of detecting changes in Virtual-DOM through DOM-Diff and updating views.

II. Purpose of DOM-Diff

After experiencing some operations or other influences, some nodes on Virtual-DOM have changed. At this time, the real DOM nodes on the page are consistent with the old DOM tree (because the old DOM tree is rendered according to the old Virtual-DOM). The function of DOM-Diff is to find out the difference between the old and the new Virtual-DOM, and render these changes to the real DOM section. Point it up.

3. Basic Algorithmic Description of DOM-Diff

In order to improve efficiency, we need to use the basic batch thinking in the algorithm. That is to say, we first find out the differences of all nodes by traversing Virtual-DOM, record them in a patch package, and then update the view by executing addPatch() logic together with the patch package after traversing. The time complexity of the complete tree comparison algorithm is too high. The algorithm used in DOM-Diff only compares the nodes in the new and old trees at the same level, ignoring the cross-level comparison.

Calendar and index each node

  • The tagName or key of the old and new nodes are different

    It means that the old node needs to be replaced, and its sub-nodes need not be traversed. In this case, the processing is simple and rough, and the patching phase will directly replace the whole old node with the new node.

  • The old and new nodes tagName and key are the same

    Start checking attributes:

    • Check for attribute deletion
    • Check for property modifications
    • Check for new attributes
    • Add changes to the patches patch package as type markers for attribute changes
  • After completing the comparison, the Virtual-DOM changes are rendered to the real DOM nodes according to the patches patch package.

Simple Implementation of DOM-Diff

4.1 Expected effect

Let's first build two Virtual-DOM with differences to simulate the state change of virtual DOM:

<!--used DOM tree-->
<div class="main" id="body">
  <div class="sideBar">
     <ul class="sideBarContainer" cprop="1">
         <li class="sideBarItem">page1</li>
         <li class="sideBarItem">page2</li>
         <li class="sideBarItem">page3</li>
     </ul>
  </div>
  <div class="mainContent">
      <div class="header">header zone</div>
      <div class="coreContent">
           <div fx="1">flex1</div>
           <div fx="2">flex2</div>
      </div>
      <div class="footer">footer zone</div>
  </div>
</div>

<!--new DOM tree-->
<div class="main" id="body">
  <div class="sideBar">
     <ul class="sideBarContainer" cprop="1" ap='test'>
         <li class="sideBarItem" bp="test">page4</li>
         <li class="sideBarItem">page5</li>
         <div class="sideBarItem">FromLiToDiv</div>
     </ul>
  </div>
  <div class="mainContent">
      <div class="header">header zone</div>
      <div class="coreContent">
           <div fx="3">flex1</div>
           <div fx="2">flex2</div>
      </div>
      <div class="footer">footer zone</div>
  </div>
</div>

If the DOM-Diff algorithm works properly, the following differences should be detected:

1.ul Added to the label ap="test"attribute
2.li The first tag modifies the text node content and adds new attributes
3.The second node modifies the content
4.li The third element is replaced by div element
5.flex1 On the label fx Attribute values have changed
/*Because the depth-first traversal adds index codes to the nodes in order of access, the above changes are translated into tags similar to the following*/
patches = {
    '2':[{type:'New attribute',propName:'ap',value:'test'}],
    '3':[{type:'New attribute',propName:'bp',value:'test'},{type:'Modify content',value:'page4'}],
    '4':[{type:'Modify content',value:'page5'}],
    '5':[{type:'Replacement elements',node:{tag:'div',.....}}]
    '9':[{type:'modify attribute',propName:'fx',value:'3'}]
} 

4.2 DOM-Diff Code

The code simplifies the judgment logic, so it is not very long. It is written directly together for easy learning. Details are written directly in the code in the form of annotations.

The omitted logical part is mainly for listing formal elements such as multiple li. It includes not only the addition and deletion of tags themselves, but also the sorting and element tracking. The scenario is more complex and will be described in subsequent blog posts.

domdiff.js:

/**
 * DOM-Diff Main frame
 */

/**
 * #define Define the type of patch
 */
let PatchType = {
    ChangeProps: 'ChangeProps',
    ChangeInnerText: 'ChangeInnerText',
    Replace: 'Replace'
}

function domdiff(oldTree, newTree) {
   let patches = {}; //Patch Pack for Recording Differences
   let globalIndex = 0; //Adding indexes to nodes during traversal makes it easy to find nodes when patching.
   dfsWalk(oldTree, newTree, globalIndex, patches);//patches are recursive in the form of addresses, so no return value is required.
   console.log(patches);
   return patches;
}

//Depth-first traversal tree
function dfsWalk(oldNode, newNode, index, patches) {
    let curPatch = [];
    let nextIndex = index + 1;
    if (!newNode) {
        //Do nothing if no new node is passed in
    }else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key){
        //The nodes are the same and the attributes are judged (undefined and equal when key s are not written)
        let props = diffProps(oldNode.props, newNode.props);
        if (props.length) {
            curPatch.push({type : PatchType.ChangeProps, props});
        }
        //If there are subtrees, traverse them
        if (oldNode.children.length>0) {
            if (oldNode.children[0] instanceof Element) {
                //If it is a child node, it is handled recursively
                nextIndex = diffChildren(oldNode.children, newNode.children, nextIndex, patches);
            } else{
                //Otherwise, it is treated as a text node comparison value.
                if (newNode.children[0] !== oldNode.children[0]) {   
                    curPatch.push({type : PatchType.ChangeInnerText, value:newNode.children[0]})
                }
            }
        }
    }else{
        //Node tagName or key is different
        curPatch.push({type : PatchType.Replace, node: newNode});
    }

    //Add the collected changes to the patch pack
    if (curPatch.length) {
        if (patches[index]) {
            patches[index] = patches[index].concat(curPatch);
        }else{
            patches[index] = curPatch;
        }
    }

    //To track the node index, you need to return the index
    return nextIndex;
}

//Contrast node attributes
/**
 * 1.Traversing through the old sequence to check whether there is property deletion or modification
 * 2.Traversing through new sequences, checking for new attributes
 * 3.Definition: type = DEL deletion
 *         type = MOD modify
 *         type = NEW Newly added
 */
function diffProps(oldProps, newProps) {

    let propPatch = [];
    //Traversing through old attributes to check deletion and modification
    for(let prop of Object.keys(oldProps)){
        //If the node is deleted
       if (newProps[prop] === undefined) {
          propPatch.push({
              type:'DEL',
              propName:prop
          });
       }else{
         //Nodes exist to determine whether there is a change
         if (newProps[prop] !== oldProps[prop]) {
            propPatch.push({
                type:'MOD',
                propName:prop,
                value:newProps[prop]
            });
         }
       } 
    }

    //Traversing through new attributes to check for new attributes
    for(let prop of Object.keys(newProps)){
        if (oldProps[prop] === undefined) {
            propPatch.push({
                type:'NEW',
                propName:prop,
                value:newProps[prop]
            })
        }
    }
    
    //Returns a patch package for property checking
    return propPatch;
}

/**
 * Traversing subnodes
 */
function diffChildren(oldChildren,newChildren,index,patches) {
    for(let i = 0; i < oldChildren.length; i++){
        index = dfsWalk(oldChildren[i],newChildren[i],index,patches);
    }
    return index;
}

Run domdiff() to compare the results of two trees:

We can see that the results are consistent with our expectations.

4.3 Update views based on patch packs

After you get the patch package, you can update the view. The algorithm logic of updating the view is as follows:

Once again, the Virtual-DOM is traversed in depth first. If a patched node is encountered, the change DOM () method is called to modify the page. Otherwise, the index is added to continue searching.

addPatch.js:

/**
 * Update views according to patch packs
 */

function addPatch(oldTree, patches) {
   let globalIndex = 0; //Adding indexes to nodes during traversal makes it easy to find nodes when patching.
   dfsPatch(oldTree, patches, globalIndex);//patches are recursive in the form of addresses, so no return value is required.
}

//Patching Deep Traversal Nodes
function dfsPatch(oldNode, patches, index) {
    let nextIndex = index + 1;
    //If there is a patch, patch it.
    if (patches[index] !== undefined) {
        //Refresh the DOM corresponding to the current virtual node
        changeDOM(oldNode.el,patches[index]);
    }
    //Recursive traversal if there are self-child nodes and child nodes are Element instances
    if (oldNode.children.length && oldNode.children[0] instanceof Element) {
        for(let i =0 ; i< oldNode.children.length; i++){
           nextIndex = dfsPatch(oldNode.children[i], patches, nextIndex);
        }
    }
    return nextIndex;
}

//Modify DOM according to patch type
function changeDOM(el, patches) {
    patches.forEach(function (patch, index) {
        switch(patch.type){
            //change attributes
            case 'ChangeProps':
               patch.props.forEach(function (prop, index) {
                   switch(prop.type){
                      case 'NEW':
                      case 'MOD':
                          el.setAttribute(prop.propName, prop.value);
                      break;
                      case 'DEL':
                          el.removeAttribute(prop.propName);
                      break;
                   }
               })
            break;
            //Change the content of text nodes
            case 'ChangeInnerText':
                 el.innerHTML = patch.value;
            break;
            //Replacement of DOM nodes
            case 'Replace':
                let newel = h(patch.node.tag, patch.node.props, patch.node.children).render(); 
                el.parentNode.replaceChild(newel , el);
        }
    })
}

In the event listener function of the page test button, when DOM-Diff is executed and addPatch() is called, you can see that the new DOM tree has been rendered to the page:

Summary

The idea of DomDiff algorithm is not particularly difficult to understand. The main difficulty in handwriting code lies in the tracing of node index. In the addPatch() phase, it is necessary to match the node index number in the patch package with the old Virtual-DOM tree. There are two basic knowledge points involved here:

  1. Functional parameter refers to the object type when it is imported. Modifying the object attribute in the function will affect the external scope of the function, and the patches patch package takes advantage of this basic feature to transfer the patches object reference generated from the top down to the outermost layer. The function used for recursion in depth-first traversal has a formal parameter to represent patches, so that in traversal, regardless of whether it is in depth-first traversal. The same patches are shared across all layers.
  2. The second difficulty lies in node index tracing, for example, there are three nodes in the second layer, the first one is labeled 2, and the number of the second node in the same layer depends on how many numbers the child nodes of the first node consume. So the code return s a number in the dfswalk() iteration function. The message to the parent caller is that I and all my child nodes have traversed. The index of the last node (or the next available node) is XXX, so that the traversal function can correctly mark and track the index of the node. It is easier for readers who feel that this part is not well understood to draw the process of depth-first traversal by themselves.
  3. In this article, we only list some basic scenarios on the comparison strategy of nodes. The comparison of nodes related to the list is relatively complex, which will be described in the following blog posts.

Posted by bbmak on Wed, 20 Mar 2019 21:15:27 -0700