This article is from “The meaning and implementation of VirtualDOM”, if you feel good, welcome to Github repository a star.

Abstract

With the rise of React, the principles and implementation of the Virtual DOM began to appear in interviews and community articles. In fact, this approach has been implemented in D3.js as early as the rapid establishment of the React ecosystem, which has officially entered the perspective of the majority of developers.

Before you start, ask a few questions to guide your thoughts, and these questions will be solved gradually in different sections:

  • 🤔️ how to understand VDom?
  • 🤔️ how to express VDom?
  • 🤔️ How to compare VDom trees and update them efficiently?

⚠️ the code and renderings are stored at github.com/dongyuanxin.

How to understand VDom?

Once upon a time, the front end used to do nothing but update the interface view based on data state updates. People are increasingly aware that frequently updating the DOM for complex view interfaces can cause backflow or redrawing, resulting in performance degradation and page stalling.

Therefore, we need ways to avoid frequently updating the DOM tree. The idea is simple, that is: compared to the DOM gap, only partial nodes need to be updated, rather than updating a tree. The realization of this algorithm is based on the need to traverse the NODES of the DOM tree to compare updates.

For faster processing, instead of using DOM objects, you use JS objects, which act like a layer of caching between JS and DOM.

How do I represent VDom?

With the help of ES6 class, VDom is more semantic. A basic VDom requires a tag name, tag attributes, and child nodes, as follows:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children; }}Copy the code

To make it easier to call (without writing new every time), encapsulate the function that returns the instance:

function el(tagName, props, children) {
  return new Element(tagName, props, children);
}
Copy the code

At this point, if you want to express the following DOM structure:

<div class="test">
  <span>span1</span>
</div>
Copy the code

With VDom:

// The elements of the child node array can be text or VDom instances
const span = el("span", {},"span1"]);
const div = el("div", { class: "test" }, [span]);
Copy the code

Later, when comparing and updating the two VDom trees, it involves rendering the VDom as a real Dom node. Therefore, add the render method to class Element:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }

  render() {
    const dom = document.createElement(this.tagName);
    // Set the tag attribute value
    Reflect.ownKeys(this.props).forEach(name= >
      dom.setAttribute(name, this.props[name])
    );

    // Update child nodes recursively
    this.children.forEach(child= > {
      const childDom =
        child instanceof Element
          ? child.render()
          : document.createTextNode(child);
      dom.appendChild(childDom);
    });

    returndom; }}Copy the code

How do I compare VDom trees and update them efficiently?

The usage and meaning of VDom have been explained previously. Multiple VDom will form a virtual DOM tree. All that remains to be done is to add, delete and change nodes in the tree according to different situations. This process is divided into diff and patch:

  • Diff: Recursively compares the node differences at corresponding positions of two VDom trees
  • Patch: Updates nodes based on differences

At present, there are two ideas. One is to diff once, record all the differences, and then conduct patch uniformly. The other is to perform patch at the same time as DIff. In comparison, the second method has one less recursive query and does not require too many objects to be constructed. The following is the second approach.

Meaning of variable

Put the diff and patch processes into the updateEl method, which is defined as follows:

/** * * @param {HTMLElement} $parent * @param {Element} newNode * @param {Element} oldNode * @param {Number} index */
function updateEl($parent, newNode, oldNode, index = 0) {
  // ...
}
Copy the code

All variables that start with $represent the real DOM.

The index argument represents the index position of oldNode in the array of all the children of $parent.

Case 1: A node is added

If oldNode is undefined, newNode is a new DOM node. Append it directly to the DOM node:

function updateEl($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(newNode.render());
  }
}
Copy the code

Case 2: A node is deleted

If newNode is undefined, it indicates that there is no node in the current position in the new VDom tree, so it needs to be deleted from the actual DOM. $parent.removechild () is called to get a reference to the deleted element using the index argument:

function updateEl($parent, newNode, oldNode, index = 0) {
  if(! oldNode) { $parent.appendChild(newNode.render()); }else if (!newNode) {
    $parent.removeChild($parent.childNodes[index]);
  }
}
Copy the code

Case 3: Change node

Comparing oldNode and newNode, there are three cases, all of which can be considered changes:

  1. The node type changes: the text becomes vDOM; Vdom becomes text
  2. The old and new nodes are text, and the content changes
  3. The attribute value of the node changes

First, use Symbol to better semantically declare these three changes:

const CHANGE_TYPE_TEXT = Symbol("text");
const CHANGE_TYPE_PROP = Symbol("props");
const CHANGE_TYPE_REPLACE = Symbol("replace");
Copy the code

There is no readily available API for batch updating when node attributes change. So encapsulate the replaceAttribute, mapping the attributes of the new VDOM directly to the DOM structure:

function replaceAttribute($node, removedAttrs, newAttrs) {
  if(! $node) {return;
  }

  Reflect.ownKeys(removedAttrs).forEach(attr= > $node.removeAttribute(attr));
  Reflect.ownKeys(newAttrs).forEach(attr= >
    $node.setAttribute(attr, newAttrs[attr])
  );
}
Copy the code

Write the checkChangeType function to determine the type of change; Return null if there is no change:

function checkChangeType(newNode, oldNode) {
  if (
    typeofnewNode ! = =typeofoldNode || newNode.tagName ! == oldNode.tagName ) {return CHANGE_TYPE_REPLACE;
  }

  if (typeof newNode === "string") {
    if(newNode ! == oldNode) {return CHANGE_TYPE_TEXT;
    }
    return;
  }

  const propsChanged = Reflect.ownKeys(newNode.props).reduce(
    (prev, name) = >prev || oldNode.props[name] ! == newNode.props[name],false
  );

  if (propsChanged) {
    return CHANGE_TYPE_PROP;
  }
  return;
}
Copy the code

In updateEl, perform the corresponding processing based on the change type returned by checkChangeType. If the type is empty, no processing is done. The specific logic is as follows:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if(! oldNode) { $parent.appendChild(newNode.render()); }else if(! newNode) { $parent.removeChild($parent.childNodes[index]); }else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if(changeType === CHANGE_TYPE_PROP) { replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props); }}}Copy the code

Case 4: Diff recursively on child nodes

If cases 1, 2, and 3 are not hit, then the current old and new nodes themselves have not changed. At this point, we need to traverse their (Virtual Dom) children array (Dom child nodes), recursive processing.

The code implementation is very simple:

function updateEl($parent, newNode, oldNode, index = 0) {
  let changeType = null;

  if(! oldNode) { $parent.appendChild(newNode.render()); }else if(! newNode) { $parent.removeChild($parent.childNodes[index]); }else if ((changeType = checkChangeType(newNode, oldNode))) {
    if (changeType === CHANGE_TYPE_TEXT) {
      $parent.replaceChild(
        document.createTextNode(newNode),
        $parent.childNodes[index]
      );
    } else if (changeType === CHANGE_TYPE_REPLACE) {
      $parent.replaceChild(newNode.render(), $parent.childNodes[index]);
    } else if(changeType === CHANGE_TYPE_PROP) { replaceAttribute($parent.childNodes[index], oldNode.props, newNode.props); }}else if (newNode.tagName) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    for (let i = 0; i < newLength || i < oldLength; ++i) { updateEl( $parent.childNodes[index], newNode.children[i], oldNode.children[i], i ); }}}Copy the code

Results observation

Will github.com/dongyuanxin… Clone the code to local, Chrome opens index.html.

New DOM node.gif:

Update text content.gif:

Change node properties.gif:

⚠️ If you have a slow Internet connection, please go to github warehouse

Refer to the link

  • How to write your own Virtual DOM