Virtual Dom Explanation-

Keywords: Javascript React Attribute Vue

In the first article, the basic implementation of virtual DOM is explained. A brief review shows that virtual DOM is a virtual Node tree described by json data, and its real DOM nodes are generated by render function. And add it to its corresponding element container. While creating a real DOM node, register events for it and add some subsidiary attributes.

Virtual Dom Details (I)

As mentioned in the previous article, when the state changes, we compare the newly rendered JavaScript object with the JavaScript object of the old virtual DOM, recording the difference between the two trees, reflecting the difference to the real DOM structure, and only operate the change of the difference part when we finally operate the real DOM. However, the last article only mentioned a simple sentence, but did not carry out a substantive implementation, this article mainly describes how the virtual DOM is updated. Let's get started. O()

DIFF algorithm is used to update the virtual DOM. I think most of the small partners should have heard of this word. DIFF is the core part of the whole virtual DOM, because it is impossible to replace the whole DOM tree when the state of the virtual DOM node changes. If so, two DOM nodes will appear. Operations are nothing more than a great impact on performance. If so, it is better to operate DOM directly.

In the first article, render is used to render the virtual DOM node tree, but only one thing is done in render function. It is just the initialization of the virtual DOM. In fact, in retrospect, whether new operation or modification operation, it should be done through render function in react. All DOM rendering is done through render function, so this conclusion is drawn.

//  Rendering Virtual DOM
//    Virtual DOM Node Tree
//    Containers that carry DOM nodes, parent elements
function render(vnode,container) {
  //  First render
  mount(vnode,container);
};

Since both update and create operations are done through render functions, how should we distinguish between new or updated current operations in the method? After all, in react we don't give a clear identification to tell its method which operation is currently being performed. There are two parameters in executing render function, one is the incoming vnode tree, and the other is the container carrying real DOM nodes. In fact, we can mount its virtual DOM node tree in its container. If the node tree exists in the container, it is an update operation, and vice versa, it is a new operation.

//  Rendering Virtual DOM
//    Virtual DOM Node Tree
//    Containers that carry DOM nodes, parent elements
function render(vnode, container) {

  if (!container.vnode) {
    //  First render
    mount(vnode, container);
  } else {
    //  Old virtual DOM nodes
    //  New DOM Node
    //  Containers Bearing DOM Nodes
    patch(container.vnode, vnode, container);
  }
  container.vnode = vnode;
};

Now that the operation of render function has been determined, the next step should be taken. If you want to update, you must know the following parameters: what is the original virtual DOM node and what is the new virtual DOM. The original virtual DOM node has been saved in the parent container and can be used directly.

//  Update function
//    Old virtual DOM nodes
//    New DOM Node
//    Containers Bearing DOM Nodes
function patch(oldVNode, newVNode, container) {
  //  VNode type of new node
  let newVNodeFlag = newVNode.flag;
  //  VNode Type of Old Node
  let oldVNodeFlag = oldVNode.flag;
  //  If the new node is not the same type as the old one
  //  If inconsistency occurs, the node changes.
  //  Replacement can be done directly.
  //  The judgement here is if one is TEXT and one is Element.
  //  Type Judgment
  if (newVNodeFlag !== oldVNodeFlag) {
    replaceVNode(oldVNode, newVNode, container);
  }
  //  Because Element and Text are created at new time using two functions to operate on
  //  It's the same when updating.
  //  Different operations should also be carried out for different modifications.
  //  If the HTML of the new node is the same as that of the old node
  else if (newVNodeFlag == vnodeTypes.HTML) {
    //  Replacement element operation
    patchMethos.patchElement(oldVNode, newVNode, container);
  }
  //  If the TEXT of the new node is the same as that of the old node
  else if (newVNodeFlag == vnodeTypes.TEXT) {
    //  Replacement text operation
    patchMethos.patchText(oldVNode, newVNode, container);
  }
}
//  Update the VNode method set
const patchMethos = {
    //  Replacement text operation
    //    Old virtual DOM nodes
    //    New DOM Node
    //    Containers Bearing DOM Nodes
    patchText(oldVNode,newVNode,container){
        //  Get el and assign oldVNode to newVNode
        let el = (newVNode.el = oldVNode.el);
        //  If newVNode.children is not equal to oldVNode.children.
        //  Otherwise, equality has no operation and no update is required.
        if(newVNode.children !== oldVNode.children){
            //  Direct substitution
            el.nodeValue = newVNode.children;
        }
    }
};
//  Replace virtual DOM
function replaceVNode(oldVNode, newVNode, container) {
  //  Delete the old node in the original node
  container.removeChild(oldVNode.el);
  //  Rendering new nodes
  mount(newVNode, container);
}

The above method simply implements a replacement operation for Text update. Because the replacement operation of Text is relatively simple, it is not enough to complete the update of Text. Element also needs to be updated when it operates. Relatively speaking, Text updates are much simpler than Element updates. Element updates are more complex, so they are put in the back because they are more important. Haha.~

First, before you want to replace Element, you need to determine which Data data has changed before you can replace it. In this case, you need to determine the Data to be changed, and then replace the original Data before you can do the next update operation.

//  Update the VNode method set
const patchMethos = {
    //  Replacement element operation
    //    Old virtual DOM nodes
    //    New DOM Node
    //    Containers Bearing DOM Nodes
    patchElement(oldVNode,newVNode,container){
        //  If the label name of newVNode is different from that of oldVNode
        //  Since the labels are different, it's better to replace them directly. There's no need to do any other superfluous operations.
        if(newVNode.tag !== oldVNode.tag){
            replaceVNode(oldVNode,newVNode,container);
            return;
        }
        //  Update el
        let el = (newVNode.el = oldVNode.el);
        //  Getting old Data data
        let oldData = oldVNode.data;
        //  Get new Data data
        let newData = newVNode.data;
        //  If new Data data exists
        //  Update and add
        if(newData){
            for(let attr in newData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                domAttributeMethod.patchData(el,attr,oldVal,newVal);
            }
        }
        //  If the old Data exists
        //  Detection update
        if(oldData){
            for(let attr in oldData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                //  If old data exists, new data does not exist.
                //  It means deleted and needs to be updated
                if(oldVal && !newVal.hasOwnProperty(attr)){
                    //  Since the new data does not exist, the new data is passed into Null
                    domAttributeMethod.patchData(el,attr,oldVal,null);
                }
            }
        }
    }
};
//  dom Adding Attribute Method
const domAttributeMethod = {
  //  Modifying Data Data Data Method
  patchData (el,key,prv,next){
    switch(key){
      case "style":
        this.setStyle(el,key,prv,next);
        //  Add here, look at me ()
        //  Add traversal loops
        //  Loop old data
        this.setOldVal(el,key,prv,next);
        break;
      case "class":
        this.setClass(el,key,prv,next);
        break;
      default :
        this.defaultAttr(el,key,prv,next);
        break;
    }
  },
  //  Traversing through old data
  setOldVal(el,key,prv,next){
    //  Traversing through old data
    for(let attr in prv){
        //  If old data exists, new data does not exist.
        if(!next.hasOwnProperty(attr)){
            //  Direct assignment to strings
            el.style[attr] = "";
        }
    }
  },
  //  Modifying the Event Registration Method
  addEvent(el,key,prev,next){
    //  Add here, look at me ()
    //  prev exists to delete the original event and rebind the new event
    if(prev){
      el.removeEventListener(key.slice(1),prev);
    }
    if(next){
      el.addEventListener(key.slice(1),next);
    }
  }
}

In fact, the above operation only replaces the Data part, but its sub-elements are not replaced, so the sub-elements need to be replaced. The substitution sub-elements can be divided into six cases:

  1. There is only one old element.
  2. The old element is empty
  3. Old elements are multiple
  4. There is only one new element
  5. The new element is empty
  6. New elements are multiple
//  Update the VNode method set
const patchMethos = {
    //  Replacement element operation
    //    Old virtual DOM nodes
    //    New DOM Node
    //    Containers Bearing DOM Nodes
    patchElement(oldVNode,newVNode,container){
        //  If the label name of newVNode is different from that of oldVNode
        //  Since the labels are different, it's better to replace them directly. There's no need to do any other superfluous operations.
        if(newVNode.tag !== oldVNode.tag){
            replaceVNode(oldVNode,newVNode,container);
            return;
        }
        //  Update el
        let el = (newVNode.el = oldVNode.el);
        //  Getting old Data data
        let oldData = oldVNode.data;
        //  Get new Data data
        let newData = newVNode.data;
        //  If new Data data exists
        //  Update and add
        if(newData){
            for(let attr in newData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                domAttributeMethod.patchData(el,attr,oldVal,newVal);
            }
        }
        //  If the old Data exists
        //  Detection update
        if(oldData){
            for(let attr in oldData){
                let oldVal = oldData[attr];
                let newVal = newData[attr];
                //  If old data exists, new data does not exist.
                //  It means deleted and needs to be updated
                if(oldVal && !newVal.hasOwnProperty(attr)){
                    //  Since the new data does not exist, the new data is passed into Null
                    domAttributeMethod.patchData(el,attr,oldVal,null);
                }
            }
        }
        //  Added here
        //  Update child elements
        //      Old child element type
        //      New subelement types
        //      children of old child elements
        //      children of new child elements
        //      el element, container
        this.patchChildren(
            oldVNode.childrenFlag,
            newVNode.childrenFlag,
            oldVNode.children,
            newVNode.children,
            el,
        );

    },
    //  Update child elements
    //      Old child element type
    //      New subelement types
    //      children of old child elements
    //      children of new child elements
    //      el element, container
    patchChildren(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        switch(oldChildrenFlag){
            //  If the child element of the old element is one
            case childTeyps.SINGLE:
                this.upChildSingle(...arg);
                break;
            //  If the child element of the old element is empty
            case childTeyps.EMPTY:
                this.upChildEmpty(...arg);
                break;
            //  If the child elements of the old element are more than one
            case childTeyps.MULTIPLE:
                this.upChildMultiple(...arg);
                break;
        }
    },

    upChildSingle(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  Cycling new subelements
        switch(newChildrenFlag){
            //  If the child element of the new element is one
            case childTeyps.SINGLE:
                patch(oldChildren,newChildren,container);
                break;
            //  If the child element of the new element is empty
            case childTeyps.EMPTY:
                container.removeChild(oldChildren.el);
                break;
            //  If there are more than one child element of the new element
            case childTeyps.MULTIPLE:
                container.removeChild(oldChildren.el);
                for(let i = 0;i<newChildren.length;i++){
                    mount(newChildren[i],container);
                }
                break;
        }
    },

    upChildEmpty(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  Cycling new subelements
        switch(newChildrenFlag){
            //  If the child element of the new element is one
            case childTeyps.SINGLE:
                mount(newChildren,container);
                break;
            //  If the child element of the new element is empty
            case childTeyps.EMPTY:
                break;
            //  If there are more than one child element of the new element
            case childTeyps.MULTIPLE:
                container.removeChild(oldChildren.el);
                for(let i = 0;i<newChildren.length;i++){
                    mount(newChildren[i],container);
                }
                break;
        }
    },

    upChildMultiple(...arg){
        let [oldChildrenFlag,newChildrenFlag,oldChildren,newChildren,container] = arg;
        //  Cycling new subelements
        switch(newChildrenFlag){
            //  If the child element of the new element is one
            case childTeyps.SINGLE:
                for(let i = 0;i<oldChildren.length;i++){
                    container.removeChild(oldChildren[i].el);
                }
                mount(newChildren,container);
                break;
            //  If the child element of the new element is empty
            case childTeyps.EMPTY:
                for(let i = 0;i<oldChildren.length;i++){
                    container.removeChild(oldChildren[i].el);
                }
                break;
            //  If there are more than one child element of the new element
            case childTeyps.MULTIPLE:
                //  **
                //  Let's put it aside for now. Here's a comparison of all the nodes.
                //  **
                break;
        }
    }

};

The above code is messy because of nested multi-layer loops. The general logic is to use the six cases mentioned above to pair one by one and use their corresponding solutions.

In the six cases mentioned above, switch matching logic:

New data Old data
There is only one old element. There is only one new element
There is only one old element. The new element is empty
There is only one old element. New elements are multiple
The old element is empty There is only one new element
The old element is empty The new element is empty
The old element is empty New elements are multiple
Old elements are multiple There is only one new element
Old elements are multiple The new element is empty
Old elements are multiple New elements are multiple

The most complex is the last case, where there are many new and old elements, but react and vue are treated differently. The following is the diff algorithm of react.

When replacing virtual DOM, there is no need to change the order between elements. That is to say, if the original order is 123456 and the new order is 654321, the order between them will change. At this time, it needs to be changed, if the order of elements is insertion. Moreover, 192939495969 added a 9 after each number. In fact, there is no need to update at this time. In fact, the order between them is the same as before, only adding some element values. If it becomes 213456, it is time to change 12, and the rest is not necessary to make any changes. Moving. Next you need to add the most critical logic.

//  Update the VNode method set
//  Add oldMoreAndNewMore method
const patchMethos = {
  upChildMultiple(...arg) {
    let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg;
    //  Cycling new subelements
    switch (newChildrenFlag) {
      //  If the child element of the new element is one
      case childTeyps.SINGLE:
        for (let i = 0; i < oldChildren.length; i++) {
          // Traverse to delete old elements
          container.removeChild(oldChildren[i].el);
        }
        // Add new elements
        mount(newChildren, container);
        break;
      //  If the child element of the new element is empty
      case childTeyps.EMPTY:
        for (let i = 0; i < oldChildren.length; i++) {
          // Delete all child elements  
          container.removeChild(oldChildren[i].el);
        }
        break;
      //  If there are more than one child element of the new element
      case childTeyps.MULTIPLE:
        // Modified here (`') 
        this.oldMoreAndNewMore(...arg);
        break;
  },
  oldMoreAndNewMore(...arg) {
    let [oldChildrenFlag, newChildrenFlag, oldChildren, newChildren, container] = arg;
    let lastIndex = 0;
    for (let i = 0; i < newChildren.length; i++) {
      let newVnode = newChildren[i];
      let j = 0;
      //  Is the new element found?
      let find = false;
      for (; j < oldChildren.length; j++) {
        let oldVnode = oldChildren[j];
        //  The same key is the same element
        if (oldVnode.key === newVnode.key) {
          find = true;
          patch(oldVnode, newVnode, container);
          if (j < lastIndex) {
            if(newChildren[i-1].el){
              //  Need to move
              let flagNode = newChildren[i-1].el.nextSibling;
              container.insertBefore(oldVnode.el, flagNode);
            }
            break;
          }
          else {
            lastIndex = j;
          }
        }
      }
      // If the old element is not found, it needs to be added
      if (!find) {
        // Markup elements that need to be inserted
        let flagNode = i === 0 ? oldChildren[0].el : newChildren[i-1].el;
        mount(newVnode, container, flagNode);
      }
      //  Removing Elements
      for (let i = 0; i < oldChildren.length; i++) {
        //  Old node
        const oldVNode = oldChildren[i];
        //  Whether the new node key exists in the old node
        const has = newChildren.find(next => next.key === oldVNode.key);
        if (!has) {
          // If deletion does not exist
          container.removeChild(oldVNode.el)
        }
      }
    }
  }
};
//  Modify mount function
//      flagNode flags where new elements of node need to be inserted
function mount(vnode, container, flagNode) {
  //  The type of rendering label required
  let { flag } = vnode;
  //  If it is a node
  if (flag === vnodeTypes.HTML) {
    //  Call the Create Node Method
    mountMethod.mountElement(vnode, container, flagNode);
  } //  If it is text
  else if (flag === vnodeTypes.TEXT) {
    //  Call the Create Text Method
    mountMethod.mountText(vnode, container);
  };
};
//  Modify mountElement
const mountMethod = {
  //  Creating HTML Element Method
  //    Modified here (') to add flagNode parameter
  mountElement(vnode, container, flagNode) {
    //  Attribute, label name, sub-element, sub-element type
    let { data, tag, children, childrenFlag } = vnode;
    //  Real Nodes Created
    let dom = document.createElement(tag);
    //  Adding attributes
    data && domAttributeMethod.addData(dom, data);
    //  Save real DOM nodes in VNode
    vnode.el = dom;
    //  If it is not empty, it means that there are child elements.
    if (childrenFlag !== childTeyps.EMPTY) {
      //  If it's a single element
      if (childrenFlag === childTeyps.SINGLE) {
        //  Pass in the child element and the DOM node currently created as the parent element
        //  In fact, it's about mounting children into the currently created element.
        mount(children, dom);
      } //  If there are multiple elements
      else if (childrenFlag === childTeyps.MULTIPLE) {
        //  Loop subnodes and create
        children.forEach((el) => mount(el, dom));
      };
    };
    //  Adding element nodes modifies this ()
    flagNode ? container.insertBefore(dom, flagNode) : container.appendChild(dom);
  }
}

Ultimately:

const VNODEData = [
    "div",
    {id:"test",key:789},
    [
      createElement("p",{
        key:1,
        style:{
          color:"red",
          background:"pink"
        }
      },"Node 1"),
      createElement("p",{
        key:2,
        "@click":() => console.log("click me!!!")
      },"Node 2"),
      createElement("p",{
        key:3,
        class:"active"
      },"Node 3"),
      createElement("p",{key:4},"Node 4"),
      createElement("p",{key:5},"Node 5")
    ]
];
let VNODE = createElement(...VNODEData);
render(VNODE,document.getElementById("app"));

const VNODEData1 = [
    "div",
    {id:"test",key:789},
    [
      createElement("p",{
        key:6
      },"Node 6"),
      createElement("p",{
        key:1,
        style:{
          color:"red",
          background:"pink"
        }
      },"Node 1"),
      createElement("p",{
        key:5
      },"Node 5"),
      createElement("p",{
        key:2
      },"Node 2"),
      createElement("p",{
        key:4
      },"Node 4"),
      createElement("p",{
        key:3,
        class:"active"
      },"Node 3")
    ]
];

setTimeout(() => {
  let VNODE = createElement(...VNODEData1);
  render(VNODE,document.getElementById("app"));
},1000)

The above code uses a lot of logic to deal with which uses a lot of computation, comparing peer nodes between two trees. This completely reduces the complexity and does not cause any loss. Because it is not possible to move a component across the DOM tree in a web application.

In fact, both React and Vue need to assign a key attribute to their elements when rendering the list, because in diff algorithm, the original elements will be used first to adjust the location, which is also a highlight of performance optimization.

epilogue

This paper is only a simple implementation of diff algorithm, which may not meet all the requirements. The basic principle of React is the same. I hope this article can help you understand diff algorithm.

Thank you very much for reading this article for such a long time. The code in this article is too long. If there are any mistakes, please point out in the comment area. I will make corrections in time.

Posted by jbachris on Tue, 13 Aug 2019 21:13:57 -0700