As we all know, direct DOM manipulation is a performance-consuming task for the front-end. Many frameworks represented by React and Vue generally adopt Virtual DOM to solve the performance problem of frequently updating DOM caused by frequent state changes in increasingly complex Web applications. In this paper, the author realized a very simple Virtual DOM through practical operation, deepening the understanding of Virtual DOM in the mainstream front-end framework today.

Virtual DOM, the community has many excellent articles, and this article is the author to use their own way, and draw on the implementation of predecessors, in a simple way, Virtual DOM simple implementation, but does not contain snabbDOM source analysis, in the author’s final implementation, The Virtual DOM implementation of this paper is improved by referring to the principle of SNabbDOM. Interested readers can read the above articles and refer to the final code of this paper.

This article should take about 15 to 20 minutes to read.

An overview of the

This article is divided into the following aspects to describe the minimalist version of Virtual DOM core implementation:

  • The main idea of Virtual DOM
  • DOM trees are represented with JavaScript objects
  • Convert the Virtual DOM to the real DOM
    • Set the node type
    • Set the properties of a node
    • Processing of child nodes
  • Dealing with change
    • Nodes are added or deleted
    • Update the node
    • Update child nodes

The main idea of Virtual DOM

To understand what Virtual DOM means, you need to understand the DOM. DOM is an API for HTML and XML documents. DOM represents a hierarchical tree of nodes. The Virtual DOM is described abstractly by JavaScript objects. The Virtual DOM is essentially a JavaScript object, and you can map a Virtual DOM tree to a real DOM tree using the Render function.

Once the Virtual DOM is changed, a new Virtual DOM is generated, and the relevant algorithm compares the old and new Virtual DOM trees, finds the differences between them, and updates the real DOM tree with as little DOM manipulation as possible.

We can express the relationship between Virtual DOM and DOM as: DOM = Render(Virtual DOM).

DOM trees are represented with JavaScript objects

The Virtual DOM is represented as JavaScript objects and stored in memory. JSX will eventually be compiled by Babel into JavaScript objects used to represent the Virtual DOM. Consider the following JSX:

<div>
    <span className="item">item</span>
    <input disabled={true} / ></div>
Copy the code

This will eventually be compiled by Babel into the following JavaScript object:

{
    type: 'div',
    props: null,
    children: [{
        type: 'span',
        props: {
            class: 'item',
        },
        children: ['item'],}, {type: 'input',
        props: {
            disabled: true,
        },
        children: [],
    }],
}
Copy the code

Two things can be noted:

  • All DOM nodes are objects that look like this:
{ type: '... ', props: { ... }, children: { ... }, on: { ... }}Copy the code
  • The nodes in this article are represented as JavaScript strings

So how does JSX translate into JavaScript objects? Fortunately, there are many excellent tools in the community to help us do this, and due to lack of space, we won’t discuss this issue in this article. In order to facilitate you to understand the Virtual DOM more quickly, for this step, the author used open source tools to complete. The famous Babel plugin, babel-plugin-transform-react-jsx, helps us do this.

To better use babel-plugin-transform-react-jsX, we need to set up a WebPack development environment. The specific process is not described here. Students who are interested in their own implementation can go to the simple-virtual-dom to view the code.

Create a Virtual DOM using the babel-plugin-transform-react-jsx function:

function vdom(type, props, ... children) {
    return {
        type,
        props,
        children,
    };
}
Copy the code

We can then create our Virtual DOM tree with the following code:

const vNode = vdom('div'.null,
    vdom('span', { class: 'item' }, 'item'),
    vdom('input', { disabled: true}));Copy the code

Enter the above code on the console, and you can see that you have created a Virtual DOM tree represented by JavaScript objects:

Convert the Virtual DOM to the real DOM

Now that we know how to use JavaScript objects to represent our real DOM tree, how does the Virtual DOM translate into the real DOM for us?

Before we get there, there are a few caveats:

  • In the code, I will use the$The opening variable represents the real DOM object;
  • toRealDomThe function takes a Virtual DOM object as an argument and returns a real DOM object.
  • mountThe function takes two arguments: it will mount the parent node of the Virtual DOM object, a real DOM object named$parent; And the mounted Virtual DOM objectvNode;

Here is a prototype toRealDom function:

function toRealDom(vNode) {
    let $dom;
    // do something with vNode
    return $dom;
}
Copy the code

We can convert a vNode object into a real DOM object using the toRealDom method, and mount the real DOM via appendChild:

function mount($parent, vNode) {
    return $parent.appendChild(toRealDom(vNode));
}
Copy the code

Let’s deal with the vNode types, props, and children, respectively.

Set the node type

First, since we have both a text node of character type and an Element node of object type, we need to do a separate processing for type:

if (typeof vNode === 'string') {
    $dom = document.createTextNode(vNode);
} else {
    $dom = document.createElement(vNode.type);
}
Copy the code

In such a simple toRealDom function, the processing of Type is complete, so let’s look at the processing of props.

Set the properties of a node

We know that if a node has props, then props is an object. Each class of props is processed separately by traversing the props and calling the setProp method.

if (vNode.props) {
    Object.keys(vNode.props).forEach(key= > {
        setProp($dom, key, vNode.props[key]);
    });
}
Copy the code

SetProp accepts three arguments:

  • $targetThis is a real DOM object,setPropDOM manipulation will be performed on this node;
  • nameIs the attribute name;
  • valueRepresents the value of the attribute;

Now that you’ve read this, you have a general idea of what setProp needs to do. In general, for ordinary props, we attach attributes to DOM objects using setAttributes.

function setProp($target, name, value) {
    return $target.setAttribute(name, value);
}
Copy the code

But that’s not enough. Consider the following JSX structure:

<div>
    <span className="item" data-node="item" onClick={() => console.log('item')}>item</span>
    <input disabled={true} />
</div>
Copy the code

From the JSX structure above, we notice the following:

  • Due to theclassIs a JavaScript reserved word that JSX generally usesclassNameTo represent the DOM nodeclass;
  • General withonThe leading property to represent the event;
  • In addition to character types, attributes may also be Boolean values, such asdisabled, when the value istrue, add this attribute.

Therefore, setProp also needs to consider the above situation:

function isEventProp(name) {
    return /^on/.test(name);
}

function extractEventName(name) {
    return name.slice(2).toLowerCase();
}

function setProp($target, name, value) {
    if (name === 'className') { // Since class is a reserved word, JSX uses className to represent a node's class
        return $target.setAttribute('class', value);
    } else if (isEventProp(name)) { // For attributes that start with on, are events
        return $target.addEventListener(extractEventName(name), value);
    } else if (typeof value === 'boolean') { // The compatibility attribute is Boolean
        if (value) {
            $target.setAttribute(name, value);
        }
        return $target[name] = value;
    } else {
        return$target.setAttribute(name, value); }}Copy the code

Finally, there is a class of properties that are our custom properties. For example, the state transfer between components in the mainstream framework is passed through props. We don’t want this class of properties to be displayed in the DOM, so we need to write a function isCustomProp to check whether this property is a custom property. Since this article is only intended to implement the core idea of the Virtual DOM, for convenience, this function returns false directly in this article.

function isCustomProp(name) {
    return false;
}
Copy the code

The final setProp function:

function setProp($target, name, value) {
    if (isCustomProp(name)) {
        return;
    } else if (name === 'className') { // fix react className
        return $target.setAttribute('class', value);
    } else if (isEventProp(name)) {
        return $target.addEventListener(extractEventName(name), value);
    } else if (typeof value === 'boolean') {
        if (value) {
            $target.setAttribute(name, value);
        }
        return $target[name] = value;
    } else {
        return$target.setAttribute(name, value); }}Copy the code

Processing of child nodes

For each item in children, it is a vNode object. When the Virtual DOM is converted into the real DOM, the child node also needs to be recursively transformed. It can be imagined that, for the case of child nodes, toRealDom needs to be recursively called on the child node, as shown in the following code:

if (vNode.children && vNode.children.length) {
    vNode.children.forEach(childVdom= > {
        const realChildDom = toRealDom(childVdom);
        $dom.appendChild(realChildDom);
    });
}
Copy the code

The final toRealDom looks like this:

function toRealDom(vNode) {
    let $dom;
    if (typeof vNode === 'string') {
        $dom = document.createTextNode(vNode);
    } else {
        $dom = document.createElement(vNode.type);
    }

    if (vNode.props) {
        Object.keys(vNode.props).forEach(key= > {
            setProp($dom, key, vNode.props[key]);
        });
    }

    if (vNode.children && vNode.children.length) {
        vNode.children.forEach(childVdom= > {
            const realChildDom = toRealDom(childVdom);
            $dom.appendChild(realChildDom);
        });
    }

    return $dom;
}
Copy the code

Dealing with change

Virtual DOM was created for the fundamental reason of performance improvement. Through Virtual DOM, developers can reduce many unnecessary DOM operations to achieve optimal performance. So let’s see how the Virtual DOM algorithm achieves performance optimization by comparing the Virtual DOM tree before and after the update.

Note: This paper is the simplest implementation of the author, currently the community is generally used for the algorithm is SNabbDOM, such as Vue is a Virtual DOM based on the algorithm, interested readers can view the source code of this library, based on the Virtual DOM small example of this paper, the author finally referred to the implementation of the algorithm. This article demo portal, due to space is limited, interested readers can study.

To handle the change, we first declare an updateDom function that takes the following four parameters:

  • $parentIs the parent node to be mounted.
  • oldVNodeAnd the oldVNodeObject;
  • newVNodeThe newVNodeObject;
  • indexIs used when updating child nodes. Indicates the number of child nodes that are being updated. The default value is 0.

The function prototype is as follows:

function updateDom($parent, oldVNode, newVNode, index = 0) {}Copy the code

Nodes are added or deleted

Add a new node to the DOM tree using appendChild () :

Translated into code expressed as:

// There is no old node, add a new node
if(! oldVNode) {return $parent.appendChild(toRealDom(newVNode));
}
Copy the code

Similarly, to remove an old node, we use removeChild. In this case, we should remove the old node from the real DOM, but the problem is that we can’t get the node directly from this function. We need to know where the node is in the parent node. $parent. ChildNodes [index] = $parent. ChildNodes [index] = $parent.

Translated into code expressed as:

const $currentDom = $parent.childNodes[index];

// If there is no new node, delete the old node
if(! newVNode) {return $parent.removeChild($currentDom);
}
Copy the code

Update the node

The core of the Virtual DOM is how to update nodes efficiently. Let’s take a look at updating nodes.

If the text node is the same as the new one, we don’t need to update the DOM. In updateDom, we can simply return:

// All text nodes are unchanged
if (typeof oldVNode === 'string' && typeof newVNode === 'string' && oldVNode === newVNode) {
    return;
}
Copy the code

Next, consider whether the node really needs to be updated. As shown in the figure, the type of a node has changed from SPAN to div, which obviously requires us to update the DOM:

We need to write a function isNodeChanged to help us determine if the old node and the new node are really the same, and if they are not, we need to replace the node:

function isNodeChanged(oldVNode, newVNode) {
    // One is a textNode and one is an element
    if (typeofoldVNode ! = =typeof newVNode) {
        return true;
    }

    // Both textNodes compare whether the text has changed
    if (typeof oldVNode === 'string' && typeof newVNode === 'string') {
        returnoldVNode ! == newVNode; }// Both element nodes compare whether the node type has changed
    if (typeof oldVNode === 'object' && typeof newVNode === 'object') {
        return oldVNode.type !== newVNode.type;
    }
}
Copy the code

In updateDom, if the node type changes, the node is replaced directly, as shown in the following code. The old DOM node is removed and the new DOM node is added by calling replaceChild:

if (isNodeChanged(oldVNode, newVNode)) {
    return $parent.replaceChild(toRealDom(newVNode), $currentDom);
}
Copy the code

But that’s far from the end. Consider this:

<! -- old -->
<div class="item" data-item="old-item"></div>
Copy the code
<! -- new -->
<div id="item" data-item="new-item"></div>
Copy the code

Vnode. type is both ‘div’, but the attributes of the node have changed. In addition to updating the DOM for the change of node type, the DOM for the change of node attributes also needs to be updated accordingly.

Similarly, we write an isPropsChanged function to determine whether the attributes of the old and new nodes have changed:

function isPropsChanged(oldProps, newProps) {
    // The props must have changed
    if (typeofoldProps ! = =typeof newProps) {
        return true;
    }

    // props is the object
    if (typeof oldProps === 'object' && typeof newProps === 'object') {
        const oldKeys = Object.keys(oldProps);
        const newkeys = Object.keys(newProps);
        // The number of props is different
        if(oldKeys.length ! == newkeys.length) {return true;
        }
        // If the number of props is the same, check whether there are any inconsistent props
        for (let i = 0; i < oldKeys.length; i++) {
            const key = oldKeys[i]
            if(oldProps[key] ! == newProps[key]) {return true; }}// The default is unchanged
        return false;
    }

    return false;
}
Copy the code

IsPropsChanged checks whether the props of the old node and the props of the new node are of the same type, and whether the old node has the same props and the new node has a new attribute, or vice versa: The props of the new node is null, and the properties of the old node are deleted. If the type is inconsistent, the attribute must be updated.

Then, considering that the node has props before and after the update, we need to judge whether the props before and after the update are consistent, that is, whether the two objects are congruent, and we can traverse it. If the properties are not equal, the props changes, and the changes need to be handled.

Now, let’s go back to our updateDom function and see that the update to the Virtual DOM node props is applied to the real DOM.

// The type of the virtual DOM is unchanged
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
if (isPropsChanged(oldProps, newProps)) {
    const oldPropsKeys = Object.keys(oldProps);
    const newPropsKeys = Object.keys(newProps);

    // If the new node has no attributes, remove the attributes of the old node
    if (newPropsKeys.length === 0) {
        oldPropsKeys.forEach(propKey= > {
            removeProp($currentDom, propKey, oldProps[propKey]);
        });
    } else {
        // Get all props to traverse, adding/deleting/modifying properties
        const allPropsKeys = new Set([...oldPropsKeys, ... newPropsKeys]);
        allPropsKeys.forEach(propKey= > {
            // Attributes are removed
            if(! newProps[propKey]) {return removeProp($currentDom, propKey, oldProps[propKey]);
            }
            // Attributes changed/increased
            if(newProps[propKey] ! == oldProps[propKey]) {returnsetProp($currentDom, propKey, newProps[propKey]); }}); }}Copy the code

The code above is also very easy to understand, if you find that the props have changed, then traverse each item of the old props. Remove non-existent attributes and add the new attributes to the updated DOM tree:

  • First, if the new node has no attributes, traverse removes all the attributes of the old node, in this case, by callingremovePropDelete it.removePropwithsetPropCorrespondingly, due to the limited space of this article, THE author will not elaborate too much here;
function removeProp($target, name, value) {
    if (isCustomProp(name)) {
        return;
    } else if (name === 'className') { // fix react className
        return $target.removeAttribute('class');
    } else if (isEventProp(name)) {
        return $target.removeEventListener(extractEventName(name), value);
    } else if (typeof value === 'boolean') {
        $target.removeAttribute(name);
        $target[name] = false;
    } else{ $target.removeAttribute(name); }}Copy the code
  • If the new node has attributes, then get the old node and all the attributes of the new node, traversal all the attributes of the old node, if the attribute is not in the new node, then the attribute is deleted. If the new node does not match the old node attribute/or is a new attributesetPropAdd new attributes to real DOM nodes.

Update child nodes

Finally, similar to toRealDom, in updateDom we should also deal with all child nodes by recursively calling updateDom to compare vNodes of all child nodes one by one to see if they are updated. Once the VNode is updated, the real DOM also needs to be re-rendered:

// The root nodes are the same, but the children are different
if (
    (oldNode.children && oldNode.children.length) ||
    (newNode.children && newNode.children.length)
) {
    for (let i = 0; i < oldNode.children.length || i < newNode.children.length; i++) { updateDom($currentDom, oldNode.children[i], newNode.children[i], i); }}Copy the code

Far from over

The above is the simplest Virtual DOM code implemented by the author, but there is a world of difference between this and the Virtual DOM algorithm used by the community. The author gives a simple example here:

<! -- old -->
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
Copy the code
<! -- new -->
<ul>
    <li>5</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
</ul>
Copy the code

For the updateDom function implemented in the above code, if the DOM structure before and after the update is as shown above, all five Li nodes will be re-rendered, which is obviously a waste of performance. However, SNabbDOM solves the above problems better by moving nodes. Due to the limited space of this paper and the community also has many articles on the analysis of the Virtual DOM algorithm, the author will not elaborate too much in this paper. Interested readers can go to their own research. Based on the examples in this paper, the author also realized the final version by referring to the SNabbDOM algorithm. Interested readers can check the final version of the example in this paper