The virtual DOM has lived and died

Virtual DOM is also called VDOM. Virtual DOM is not a new thing. There have been many articles about virtual DOM on the Internet for many years. However, React promotes virtual DOM, and VUe2.0 also introduces virtual DOM, which shows that virtual DOM plays an important role in the front end.

To put it simply, the virtual DOM is a DOM structure represented in a data format. There is no real append to the DOM, so it is called the virtual DOM.

What does the virtual DOM do

The benefits of using the virtual DOM are obvious:

Interacting with the BROWSER to manipulate the DOM is not nearly as efficient as manipulating data structures. Manipulating data structures means changing “virtual DOM objects,” a process that is much faster than modifying the real DOM.

However, using the virtual DOM does not reduce the number of DOM manipulations, because the virtual DOM is eventually mounted to the browser as a real DOM node, but accurately retrievals the smallest and most necessary set of DOM manipulations.

In this way, we abstracted the DOM, calculated the minimum difference between the view updates each time through the diff algorithm of the virtual DOM, and then rendered/updated the real DOM according to the minimum difference, which is undoubtedly more reliable and higher performance.

Creating a virtual DOM

With all that said, how do you actually create a virtual DOM? We implement a simplified version of the virtual DOM by copying the ideas of some of the major virtual DOM libraries.

The existing DOM structure is as follows:

<ul id="ul1">
  <li class="li-stl1">item1</li>
  <li class="li-stl1">item2</li>
  <li class="li-stl1">item3</li>
</ul>
Copy the code

Now to express it in JS, we build an object structure like this:

const myVirtualDom = {
  tagName: "ul".attributes: {
    id: "ul1",},children: [{tagName: "li".attributes: { class: "li-stl1" }, children: ["item1"] {},tagName: "li".attributes: { class: "li-stl1" }, children: ["item2"] {},tagName: "li".attributes: { class: "li-stl1" }, children: ["item3"]},]};Copy the code
  • TagName indicates the actual DOM tag type.
  • Attributes is an object that represents all the attributes on a real DOM node;
  • Children correspond to childNodes of the real DOM, each of which has a similar structure.

With the data structure defined, you now need a method (class) that can generate a virtual DOM structured this way. For producing the virtual DOM:

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children; }}// Encapsulates the createVirtualDom method, internally calling the Element constructor
function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}
Copy the code

The above virtual DOM can be generated as follows:

const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1"},"item1"]),
  createVirtualDom("li", { class: "li-stl1"},"item2"]),
  createVirtualDom("li", { class: "li-stl1"},"item3"]),]);Copy the code

The results are shown below:

The data format of the generated virtual DOM object is more as defined. Isn’t that easy? With the virtual DOM object generated, we continue the process of converting the virtual DOM into a real DOM node.

The virtual DOM becomes the real DOM

The setAttribute method is used to set the attributes of a DOM node.

Parameter 1: DOM node parameter 2: attribute name Parameter 3: attribute value

const setAttribute = (node, key, value) = > {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        // Non-input && textarea uses setAttribute to set the value attribute
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break; }};Copy the code

Add the render instance method to the virtual DOM class. The function of this method is to generate a real DOM fragment from the virtual DOM:

class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    // Iterate over the child node, recursively calling Render if child is also a virtual node, or create a text node if it is a string
    children.forEach((child) = > {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    returnelement; }}Copy the code

After creating tags according to tagName, use the tool method setAttribute to create attributes. Call render recursively for each type of children, if VirtualDom instance; Render the content until the text node type is encountered.

Real DOM rendering

With the actual DOM node fragment, we strike while the iron is hot and render the actual DOM node to the browser. Implement the renderDOM method:

const renderDom = (element, target) = > {
  target.appendChild(element);
};
Copy the code

The complete code so far is as follows:

// Add attribute methods
const setAttribute = (node, key, value) = > {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break; }};// Virtual DOM class
class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) = > {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    returnelement; }}// Generate a virtual DOM method
function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

// Render the actual DOM node to the browser
const renderDom = (element, target) = > {
  target.appendChild(element);
};

// Execute method to generate virtual DOM
const myVirtualDom = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1"},"item1"]),
  createVirtualDom("li", { class: "li-stl1"},"item2"]),
  createVirtualDom("li", { class: "li-stl1"},"item3"]),]);// Execute the render method of the virtual DOM to convert the virtual DOM to the real DOM
const realDom = myVirtualDom.render();

// Render the real DOM onto the browser
renderDom(realDom, document.body);
Copy the code

This completes the process from creating the virtual DOM to converting it to the real DOM and rendering it to the browser, which is not too difficult to implement.

Virtual DOM diff

With the above implementation, you can produce a virtual DOM and convert it into a real DOM rendering in the browser. The virtual DOM is not immutable. When a user performs a specific operation, a new virtual DOM will be produced. At the beginning, we also said that one of the advantages of the virtual DOM is that “it can accurately obtain the smallest and most necessary set of DOM operations”. So how do you figure out the difference between the two virtual DOM copies and give the browser the result that needs to be updated? This is where DOM Diff comes in. The virtual DOM is a tree structure, so we need to compare the two virtual DOM recursively, storing the changes in a variable:

Parameter 1: old virtual DOM object Parameter 2: new virtual DOM object

const diff = (oldVirtualDom, newVirtualDom) = > {
  let differences = {};

  // Recursive virtual DOM tree, calculates the difference and puts the result in the differencess object
  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  // Return the diff calculation result
  return differences;
};
Copy the code

The first two parameters of walkToDiff are two old/new virtual DOM objects to compare; The third parameter records nodeIndex, which is used when a node is deleted and starts at 0. The fourth argument is a closure variable that records the diff result. WaklToDiff is implemented as follows:

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) = > {
  let diffResult = [];

  // 1. If newVirtualDom does not exist, the node is removed. We place the object of type REMOVE into the diffResult array and record index
  if(! newVirtualDom) { diffResult.push({type: "REMOVE",
      index,
    });
  }
  // 2. If both old and new nodes are text nodes, it is a string
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    // Compare whether the text is the same, if not, record a new result
    if(oldVirtualDom ! == newVirtualDom) { diffResult.push({type: "MODIFY_TEXT".data: newVirtualDom, index, }); }}// 3. If the new and old nodes are of the same type
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // Compare attributes to see if they are the same
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if(oldVirtualDom[key] ! == newVirtualDom[key]) {// Record the differences, overwriting attributes and children directly
        diffAttributeResult[key] = newVirtualDom[key];
        // Handle the case where the attribute is deleted
        if (key === "attributes") {
          // If the diffAttributeResult does not contain an attribute in oldVirtualDom[" Attributes "], the attribute is removed in the new VDOM
          for (let attr in oldVirtualDom["attributes"]) {
            if(! diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              // If the attribute is deleted, set the value to null, which can be determined at render time
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    // New attributes that do not exist on the old node
    for (let key in newVirtualDom) {
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    // If the hierarchy is different, the difference result is logged into the diffResult array
    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    // If there are child nodes, traverse the child nodes
    oldVirtualDom.children.forEach((child, i) = > {
      walkToDiff(child, newVirtualDom.children[i], ++initialIndex, differences);
    });
  }
  // 4.else indicates that the node type is different and has been replaced directly. We will push the new result directly
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  // If no old node exists
  if(! oldVirtualDom) { diffResult.push({type: "REPLACE",
      newVirtualDom,
    });
  }

  if(diffResult.length) { differences[index] = diffResult; }};Copy the code

After adding the walkToDiff method, we tested our code as a whole:

const setAttribute = (node, key, value) = > {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break; }};class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) = > {
      let childElement =
        child instanceof VirtualDom
          ? child.render()
          : document.createTextNode(child);
      element.appendChild(childElement);
    });

    returnelement; }}function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

const renderDom = (element, target) = > {
  target.appendChild(element);
};

const diff = (oldVirtualDom, newVirtualDom) = > {
  let differences = {};

  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  return differences;
};

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) = > {
  let diffResult = [];

  // If newVirtualDom does not exist, the object is removed, and we push the diffResult variable with the object of type REMOVE and record the index
  if(! newVirtualDom) { diffResult.push({type: "REMOVE",
      index,
    });
  }
  // If both old and new nodes are text nodes, it is a string
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    if(oldVirtualDom ! == newVirtualDom) { diffResult.push({type: "MODIFY_TEXT".data: newVirtualDom, index, }); }}// If the old and new nodes are of the same type
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if(oldVirtualDom[key] ! == newVirtualDom[key]) { diffAttributeResult[key] = newVirtualDom[key];if (key === "attributes") {
          for (let attr in oldVirtualDom["attributes"]) {
            if(! diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    for (let key in newVirtualDom) {
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    oldVirtualDom.children.forEach((child, index) = > {
      walkToDiff(
        child,
        newVirtualDom.children[index],
        ++initialIndex,
        differences
      );
    });
  }
  // else indicates that the node type is different and has been replaced directly. We will push the new result directly
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if(! oldVirtualDom) { diffResult.push({type: "REPLACE",
      newVirtualDom,
    });
  }

  if (diffResult.length) {
    console.log(index); differences[index] = diffResult; }};Copy the code

The test code

const myVirtualDom1 = createVirtualDom("ul", { id: "ul1" }, [
  createVirtualDom("li", { class: "li-stl1"},"item1"]),
  createVirtualDom("li", { class: "li-stl1"},"item2"]),
  createVirtualDom("li", { class: "li-stl1"},"item3"]),]);const myVirtualDom2 = createVirtualDom("ul", { id: "ul2" }, [
  createVirtualDom("li", { class: "li-stl2"},"item4"]),
  createVirtualDom("li", { class: "li-stl2"},"item5"]),
  createVirtualDom("li", { class: "li-stl2"},"item6"]),]); diff(myVirtualDom1, myVirtualDom2);Copy the code

Calling the diff method yields the array of results after comparison:

var result = {
  "0": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { id: "ul2" },
        children: [
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item4"],
          },
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item5"],
          },
          {
            tagName: "li",
            attributes: { class: "li-stl2" },
            children: ["item6"[,},],},},],"1": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item4"]],}},"2": [{ type: "MODIFY_TEXT", data: "item4", index: 2}]."3": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item5"]],}},"4": [{ type: "MODIFY_TEXT", data: "item5", index: 4}]."5": [
    {
      type: "MODIFY_ATTRIBUTES",
      diffAttributeResult: {
        attributes: { class: "li-stl2" },
        children: ["item6"]],}},"6": [{ type: "MODIFY_TEXT", data: "item6", index: 6}]};Copy the code

The test results are in line with our expectations. At this moment, we have compared the two virtual DOM through diff method and obtained the differences in diFF method. How do YOU update the view once you get the difference? To get the differences, call patchDiff

const patchDiff = (node, differences) = > {
  // Get the index of each item in the difference array
  // We use the object form because renderDiff is passed recursively. Using an object ensures that the index value is not repeated. Using a normal value type recursively would cause problems
  let differ = { index: 0 };
  renderDiff(node, differ, differences);
};
Copy the code

The patchDiff method receives a real DOM node, which is the DOM node that needs to be updated, and a set of differences, which are combined with the results returned by the DIff method. Inside the patchDiff method, we call renderDiff:

const renderDiff = (node, differ, differences) = > {
  // Get each item in the differences array
  let currentDiff = differences[differ.index];

  // Real DOM node
  let childNodes = node.childNodes;

  // Call itself recursively
  childNodes.forEach((child) = > {
    differ.index++;
    renderDiff(child, differ, differences);
  });

  // Call the updateRealDom method to update the difference between the current node
  if(currentDiff) { updateRealDom(node, currentDiff); }};Copy the code

The renderDiff method does its own recursion and calls the updateRealDom method to update the differences between the current nodes. UpdateRealDom handles four types of diff:

const updateRealDom = (node, currentDiff) = > {
  currentDiff.forEach((dif) = > {
    switch (dif.type) {
      case "MODIFY_ATTRIBUTES":
        const attributes = dif.diffAttributeResult.attributes;
        for (let key in attributes) {
          // If it is not an element node
          if(node.nodeType ! = =1) return;

          const value = attributes[key];
          if (value) {
            setAttribute(node, key, value);
          } else {
            // When value is null, the attribute value is removed from the new virtual DOMnode.removeAttribute(key); }}break;
      case "MODIFY_TEXT":
        node.textContent = dif.data;
        break;
      case "REPLACE":
        let newNode =
          dif.newNode instanceof VirtualDom
            ? render(dif.newNode)
            : document.createTextNode(dif.newNode);
        node.parentNode.replaceChild(newNode, node);
        break;
      case "REMOVE":
        node.parentNode.removeChild(node);
        break;
      default:
        break; }}); };Copy the code

Here is a simple version of the virtual DOM library implementation, the code to test.

Complete code:

const setAttribute = (node, key, value) = > {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      let tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break; }};class VirtualDom {
  constructor(tagName, attributes = {}, children = []) {
    this.tagName = tagName;
    this.attributes = attributes;
    this.children = children;
  }

  render() {
    let element = document.createElement(this.tagName);
    let attributes = this.attributes;

    for (let key in attributes) {
      setAttribute(element, key, attributes[key]);
    }

    let children = this.children;

    children.forEach((child) = > {
      let childElement =
        child instanceof VirtualDom
          ? child.render() // If child is also a virtual node, recurse
          : document.createTextNode(child); // If it is a string, create a text node
      element.appendChild(childElement);
    });

    returnelement; }}function createVirtualDom(tagName, attributes, children) {
  return new VirtualDom(tagName, attributes, children);
}

const renderDom = (element, target) = > {
  target.appendChild(element);
};

const diff = (oldVirtualDom, newVirtualDom) = > {
  let differences = {};

  // Put the result of the recursive tree comparison in differences
  walkToDiff(oldVirtualDom, newVirtualDom, 0, differences);

  return differences;
};

let initialIndex = 0;

const walkToDiff = (oldVirtualDom, newVirtualDom, index, differences) = > {
  let diffResult = [];

  // If newVirtualDom does not exist, the object is removed, and we push the diffResult variable with the object of type REMOVE and record the index
  if(! newVirtualDom) { diffResult.push({type: "REMOVE",
      index,
    });
  }
  // If both old and new nodes are text nodes, it is a string
  else if (
    typeof oldVirtualDom === "string" &&
    typeof newVirtualDom === "string"
  ) {
    // Compare whether the text is the same, if not, record a new result
    if(oldVirtualDom ! == newVirtualDom) { diffResult.push({type: "MODIFY_TEXT".data: newVirtualDom, index, }); }}// If the old and new nodes are of the same type
  else if (oldVirtualDom.tagName === newVirtualDom.tagName) {
    // Compare attributes to see if they are the same
    let diffAttributeResult = {};

    for (let key in oldVirtualDom) {
      if(oldVirtualDom[key] ! == newVirtualDom[key]) { diffAttributeResult[key] = newVirtualDom[key];if (key === "attributes") {
          // If the diffAttributeResult does not contain an attribute in oldVirtualDom[" Attributes "], the attribute is removed in the new VDOM
          for (let attr in oldVirtualDom["attributes"]) {
            if(! diffAttributeResult["attributes"].hasOwnProperty(attr)) {
              diffAttributeResult["attributes"][attr] = "";
            }
          }
        }
      }
    }

    for (let key in newVirtualDom) {
      // New attributes that do not exist on the old node
      if (!oldVirtualDom.hasOwnProperty(key)) {
        diffAttributeResult[key] = newVirtualDom[key];
      }
    }

    if (Object.keys(diffAttributeResult).length > 0) {
      diffResult.push({
        type: "MODIFY_ATTRIBUTES",
        diffAttributeResult,
      });
    }

    // If there are child nodes, traverse the child nodes
    oldVirtualDom.children.forEach((child, index) = > {
      walkToDiff(
        child,
        newVirtualDom.children[index],
        ++initialIndex,
        differences
      );
    });
  }
  // else indicates that the node type is different and has been replaced directly. We will push the new result directly
  else {
    diffResult.push({
      type: "REPLACE",
      newVirtualDom,
    });
  }

  if(! oldVirtualDom) { diffResult.push({type: "REPLACE",
      newVirtualDom,
    });
  }

  if(diffResult.length) { differences[index] = diffResult; }};const patchDiff = (node, differences) = > {
  let differ = { index: 0 };
  renderDiff(node, differ, differences);
};

const renderDiff = (node, differ, differences) = > {
  let currentDiff = differences[differ.index];

  let childNodes = node.childNodes;

  childNodes.forEach((child) = > {
    differ.index++;
    renderDiff(child, differ, differences);
  });

  if(currentDiff) { updateRealDom(node, currentDiff); }};const updateRealDom = (node, currentDiff) = > {
  currentDiff.forEach((dif) = > {
    switch (dif.type) {
      case "MODIFY_ATTRIBUTES":
        const attributes = dif.diffAttributeResult.attributes;
        for (let key in attributes) {
          if(node.nodeType ! = =1) return;
          const value = attributes[key];
          if (value) {
            setAttribute(node, key, value);
          } else{ node.removeAttribute(key); }}break;
      case "MODIFY_TEXT":
        node.textContent = dif.data;
        break;
      case "REPLACE":
        let newNode =
          dif.newNode instanceof VirtualDom
            ? render(dif.newNode)
            : document.createTextNode(dif.newNode);
        node.parentNode.replaceChild(newNode, node);
        break;
      case "REMOVE":
        node.parentNode.removeChild(node);
        break;
      default:
        break; }}); };/ / virtual DOM1
const myVirtualDom1 = createVirtualDom("ul", { id: "ul1".class: "class1" }, [
  createVirtualDom("li", { class: "li-stl1"},"item1"]),
  createVirtualDom("li", { class: "li-stl1"},"item2"]),
  createVirtualDom("li", { class: "li-stl1"},"item3"]),]);/ / virtual DOM2
const myVirtualDom2 = createVirtualDom(
  "ul",
  { id: "ul2".style: "color:pink;" },
  [
    createVirtualDom("li", { class: "li-stl2"},"item4"]),
    createVirtualDom("li", { class: "li-stl2"},"item5"]),
    createVirtualDom("li", { class: "li-stl2"},"item6"]),]);Copy the code
  1. Convert the virtual DOM to the real DOM
var element = myVirtualDom1.render();
Copy the code

See that element is a real DOM node, as shown in the figure below

  1. Render the actual DOM node to the browser
renderDom(element, document.body);
Copy the code

At this point, the actual DOM node has been rendered to the browser, as shown below

  1. Compare two virtual DOM differences
const differences = diff(myVirtualDom1, myVirtualDom2);
Copy the code

The resulting differences are shown in the figure below

  1. Analyze the differences and update the view
patchDiff(element, differences);
Copy the code

After execution, the id is updated to UL2, the class attribute is removed, the style attribute is added, and the rest of the values are changed correctly

The end

Despite the lack of a lot of detail optimization and boundary issues, but our virtual DOM implementation is still very powerful, the basic idea and snabbDOM and other virtual DOM libraries are highly consistent.