In the past two years, front-end development has been fast, and we like to talk about some concepts, such as closures, Currization, higher-order functions, etc. React’s popularity has led to a flurry of discussions about the virtual DOM. What is the Virtual DOM, why does it exist, and what problems does it solve? This article attempts to answer this question, but before we get to the virtual DOM, we need to know and understand the real DOM.

DOM

concept

The full name of DOM is Document Object Model, which is defined as follows:

The Document Object Model (DOM) is a programming interface for HTML and XML documents. It provides a structured representation of a document and defines a way for that structure to be accessed from within the program to change the structure, style, and content of the document.

The following diagram illustrates some common interfaces and their inheritance in the DOM.

Each interface has a detailed structural description, you can see the W3C specification, here to Node interface as an example, to see its structural description (note: after the author simplified processing) :

interface Node : EventTarget {
  / * -- -- -- -- -- -- -- -- -- -- -- -- -- - -- -- -- -- -- - * /
  readonly attribute unsigned short nodeType; // Node type
  readonly attribute DOMString nodeName; // Node name
  readonly attribute NodeList childNodes; // List of child nodes
  readonly attribute Node? parentNode; / / the parent node
  readonly attribute Node? firstChild; // First child node
  readonly attribute Node? lastChild; // The last child node
  readonly attribute Node? previousSibling; // Previous sibling node
  readonly attribute Node? nextSibling; // The next sibling
  attribute DOMString? nodeValue; / / the node values
  attribute DOMString? textContent; // Text content
  / * -- -- -- -- -- -- -- -- -- -- method -- -- -- -- -- -- -- -- -- - * /
  Node insertBefore(Node node, Node? child); // Insert before a child node
  Node appendChild(Node node); // Add child nodes
  Node replaceChild(Node node, Node child); // Replace the child node
  Node removeChild(Node child); // Delete the child node
};
Copy the code

As you can see, there are many properties and methods defined for Node objects in the specification. Here are some of the most common ones. Among them, there are 12 node types in total, but 4 of them have been abandoned, only 3 are most commonly used, and the remaining 5 are not commonly used, which are listed as follows:

// Common type
const unsigned short ELEMENT_NODE = 1; // Element node
const unsigned short TEXT_NODE = 3; // Text node
const unsigned short COMMENT_NODE = 8; // Comment the node
// Not a common type
const unsigned short CDATA_SECTION_NODE = 4; / / 
       node
const unsigned short PROCESSING_INSTRUCTION_NODE = 7; / / 
       nodes (XML specific)
const unsigned short DOCUMENT_NODE = 9; / / the document node
const unsigned short DOCUMENT_TYPE_NODE = 10; / / 
       node
const unsigned short DOCUMENT_FRAGMENT_NODE = 11; / / DocumentFragment node
// Discard type
const unsigned short ATTRIBUTE_NODE = 2; / / abandoned
const unsigned short ENTITY_REFERENCE_NODE = 5; / / abandoned
const unsigned short ENTITY_NODE = 6; / / abandoned
const unsigned short NOTATION_NODE = 12; / / abandoned
Copy the code

JS operation DOM

The browser provides a set of programming apis that allow developers to manipulate DOM nodes using JAVASCRIPT, for example:

document.getElementById()
document.createElement('p')
/ / and so on...
Copy the code

It is important to note that while JS can manipulate the DOM, JS is not the only language that can manipulate the DOM. Other languages such as Python can also manipulate the DOM:

import xml.dom.minidom as m
doc = m.parse("/index.html");
p_list = doc.getElementsByTagName("para");
Copy the code

Here are some common apis that you need to be familiar with to be a good front end.

EventTarget related

  • addEventListener(type, listener): Adds event listeners
  • removeEventListener(type, listener): Removes the event listener

The Node related

  • appendChild(childNode): Adds child nodes
  • removeChild(childNode): Deletes child nodes
  • replaceChild(newNode, oldNode): Replaces the old node with the new one
  • insertBefore(newNode, refNode): Inserts a new child node before the reference node

The Document related to the

  • getElementById(id): Finds elements by ID
  • querySelector(selector): Finds elements according to the selector
  • createElement(tagName): Creates the element node
  • createTextNode(str): Creates a text node

Element related to

  • getAttribute(attrName): Gets the specified attribute value on the element
  • setAttribute(name, value): Sets an attribute value on the specified element
  • removeAttribute(attrName): Deletes an attribute of an element

Virtual DOM

concept

A virtual DOM is a plain JS object that describes the structure of the real DOM.

If you want to describe the actual DOM structure, you only need three properties:

  • tag: Node label
  • props: Node attributes
  • children: child node

For example, the HTML structure of the article is as follows:

<div id="main">
  <h2>National Day Holiday Arrangement<span style="color: red">hot</span></h2>
  <p class="article">According to the notice of The General Office of the State Council, the National Day and Mid-Autumn Festival in 2020 will be arranged as follows: October 1 (Thursday) to October 8 (Thursday), a total of 8 days off. September 27th (Sunday), October 10th (Saturday).</p>
</div>
Copy the code

The above HTML paragraph tag could have been described with the following JS object:

const vnode = {
  tag: 'div'.props: { id: 'main' },
  children: [{tag: 'h2'.children: [
        'National Day Holiday Arrangement',
        {
          tag: 'span'.props: { style: { color: 'red'}},children: ['hot'],},],}, {tag: 'p'.props: { className: 'article' },
      children: [According to the notice of The General Office of the State Council, the National Day and Mid-Autumn Festival in 2020 will be arranged as follows: October 1st (Thursday) to October 8th (Thursday), a total of eight days off. September 27th (Sunday), October 10th (Saturday). '],},],}Copy the code

In other words, once you have the VNode JS object, you can generate the above HTML node by calling the DOM API with JS. The generated code is as follows:

function createNode(vnode) {
  if (typeof vnode === 'string') return document.createTextNode(vnode)
  const { tag, props, children } = vnode
  const el = document.createElement(tag)
  for (const k in props) {
    const v = props[k]
    if (k === 'style') Object.keys(v).forEach((key) = > (el.style[key] = v[key]))
    else el.setAttribute(k, v)
  }
  children.forEach((child) = > el.appendChild(createNode(child)))
  return vnode.el = el
}
Copy the code

This means that it only takes 12 lines of code to fully restore a JS object to the real DOM, which is why objects of the vNode structure are called virtual DOM. The virtual DOM is like an architectural drawing from which an entire building can be built.

The Diff algorithm

Broadly speaking, the so-called diff algorithm is to compare two object structures and find their differences. Such as:

const a = { name: 'David'.empty: true.age: 62.salary: 100.child: { name: 'James'.age: 33.baby: { name: 'Jessica'.age: 10 },  hobby: ['football'.'reading']}}const b = { name: 'Lucy'.age: 62.java: 'good'.salary: 10.child: {  name: 'Scott'.age: 35.baby: { name: 'Jessica'.age: 11.fool: 2 }, hobby: ['shopping'.'reading']}}Copy the code

Simple objects can be distinguished by the naked eye, but complex objects cannot be distinguished by the naked eye. Use a visual library to see:

const diff = require('diff-object')
diff.saveHTML(a, b)
Copy the code

Diff algorithm for virtual DOM is to compare the difference between two VNode objects, and only peer comparison, because the structure of vnode objects is fixed, the comparison is much easier, the flow is as follows:

  • Check whether the tag name is consistent
  • See if the props properties are consistent
  • Finally, we compare children recursively

Remember the example article on National Day holiday arrangements above? Suppose a new article is published today:

<div id="main">
  <h2>Talks to buy TikTok have stalled</h2>
  <p class="article">When it was believed that TikTok's acquisition negotiation had come to an end, China adjusted and released The Catalogue of Prohibited and Restricted Export Technologies, in which the clause of "personalized information push service technology based on data analysis" in the export restriction part was interpreted by the media as directly targeting TikTok algorithm technology.<span style="color: red;">Copyright [C]. All Rights Reserved.</span></p>
</div>
Copy the code

The corresponding VNodes are as follows:

const newVnode = {
  tag: 'div'.props: { id: 'main' },
  children: [{tag: 'h2'.children: ['Talks to buy TikTok have reached an impasse'],}, {tag: 'p'.props: { className: 'article' },
      children: [
        'When it was believed that TikTok's acquisition negotiation had come to an end, China adjusted and released The Catalogue of Prohibited and Restricted Export Technologies, in which the clause of "personalized information push service technology based on data analysis" in the export restriction part was interpreted by the media as directly targeting TikTok algorithm technology. ',
        {
          tag: 'span'.props: { style: { color: 'red'}},children: [All Rights Reserved.],},],},}Copy the code

On the news website, users first browsed the article “National Day holiday arrangement” and then browsed the “Deadlock in the negotiation to acquire TikTok”. Their DOM structure is similar. In order to reduce the cost of creating DOM, the purpose of using Diff algorithm is to find out the differences and maximize the reuse of nodes. The following is a simplified version of the Diff algorithm:

// Update the real DOM structure by comparing old and new virtual nodes
function diff(v1, v2) {
  if(v1.tag ! == v2.tag)return createNode(v2)  // Tags do not create new nodes at the same time
  const el = (v2.el = v1.el) // Reuse the old node with the same label
  patchProps(v1.props, v2.props, el)
  patchChildren(v1.children, v2.children, el)
}
// Update attributes (delete old, add new)
function patchProps(p1, p2, el) {
  for (let k1 in p1) el.removeAttribute(k1)
  for (let k2 in p2) {
    const v2 = p2[k2]
    if (k2 === 'style') Object.keys(v2).forEach((k) = > (el.style[k] = v2[k]))
    else el.setAttribute(k2, v2)
  }
}
// Update the child node
function patchChildren(c1, c2, el) {
  let i = 0, node, len = Math.max(c1.length, c2.length), childNodes = Array.from(el.childNodes)
  while (i < len) {
    const v1 = c1[i], v2 = c2[i], node = v1 && childNodes[i]
    if(v1 && ! v2) el.removeChild(node)// The old node exists, but the new node does not exist
    else if(! v1 && v2) el.insertBefore(createNode(v2), node)// The old node does not exist, the new node exists, add
    else if (v1 && v2) {  // The old and new nodes do not exist
      const t1 = typeof v1, t2 = typeof v2
      if(t1 ! == t2) el.replaceChild(createNode(v2), node)// Replace the node
      else if (t1 === 'string') node.textContent = v2 // Update the text
      else diff(v1, v2) / / recursive diff
    }
    i++
  }
}
Copy the code

The real Diff algorithm is much more complex than this, but the core idea is the same, which is to maximize node reuse and reduce the overhead of creating DOM elements.

Git clone [email protected]:keliq/vdom-in-depth