Start from scratch and write a simple Virtual DOM

  Front end, javascript

As we all know, direct manipulation of DOM is a performance-intensive matter 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. This paper implements a very simple Virtual DOM for the author through practical operation, deepening the understanding of the current mainstream front-end framework of Virtual DOM.

There have been many excellent articles about Virtual DOM in the community, and this article is the author’s own way, drawing on the implementation of predecessors, to implement Virtual DOM in a simple and easy-to-understand way, but does not includesnabbdomThe source code analysis, in the author’s final implementation, referred tosnabbdomThe principle of the Virtual DOM, the implementation of this article has been improved, interested readers can read the above articles, and refer to the author of this articleFinal codeRead.

The reading time of this article is about 15~20 minutes.

summarize

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

  • The main idea of Virtual DOM
  • Representing DOM Tree with JavaScript Object
  • Convert Virtual DOM to real DOM

    • Set the type of node
    • Set the node’s properties
    • Treatment of child nodes
  • Handling changes

    • Add and delete nodes
    • Update node
    • Update child node

The main idea of Virtual DOM

To understand the meaning of Virtual DOM, you need to understand DOM first. DOM is an API for HTML documents and XML documents. DOM depicts a hierarchical node tree. By calling DOM API, developers can add, remove and modify a part of a page at will. Virtual DOM is an abstract description of Virtual DOM with JavaScript objects. The essence of Virtual DOM isJavaScript objectThroughRender function, you can map the Virtual DOM tree to a real DOM tree.

Once the Virtual DOM changes, a new Virtual DOM will be generated. The relevant algorithm will compare the new and old two Virtual DOM trees and find the difference between them, updating the real DOM tree with as few DOM operations as possible.

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

Representing DOM Tree with JavaScript Object

Virtual DOM is represented by JavaScript objects and stored in memory. Mainstream frameworks all support the use of JSX. JSX will eventually be compiled into JavaScript objects by babel to represent Virtual DOM. Consider the following JSX:

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

It 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: [],
    }],
}

We can note the following two points:

  • All DOM nodes are objects like this:
{ type: '...', props: { ... }, children: { ... }, on: { ... } }
  • The nodes in this article are represented by JavaScript strings

How does JSX translate into JavaScript objects? Fortunately, the community has many excellent tools to help us accomplish this. Due to limited space, this article will not discuss this issue for the time being. In order to facilitate people to understand Virtual DOM more quickly, the author uses open source tools to complete this step. Famous babel Plug-inbabel-plugin-transform-react-jsxHelp us finish the work.

For better usebabel-plugin-transform-react-jsx, we need to build a webpack development environment. The specific process is not described here, and students who are interested in self-realization can come here.simple-virtual-domCheck the code.

For students who do not use JSX syntax, they may not configure it.babel-plugin-transform-react-jsxThrough ourvdomFunction to create Virtual DOM:

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

Then we can create our Virtual DOM tree with the following code:

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

Entering the above code in the console, you can see that the Virtual DOM tree represented by JavaScript objects has been created:

Convert Virtual DOM to real DOM

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

Before this, we need to know a few precautions:

  • In the code, the author will$The variable at the beginning represents the real DOM object.
  • toRealDomThe function takes a Virtual DOM object as a parameter and returns a real DOM object.
  • mountThe function takes two parameters: the parent node of the Virtual DOM object will be mounted, which is a real DOM object named$parent; And the mounted Virtual DOM object.vNode;

The following istoRealDomFunction prototype of:

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

viatoRealDomMethod, we can put avNodeObject into a real DOM object, whilemountFunction passappendChildTo mount the real DOM:

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

Next, let’s deal with them separatelyvNodeThetypepropsAndchildren.

Set the type of node

First, because we have both text nodes of character type and objects of object typeelementNode, need totypeDo a separate treatment:

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

In such a simpletoRealDomFunction, righttypeThe processing is complete, let’s look at the right nextpropsTo deal with.

Set the node’s properties

We know that if a node hasprops, thenpropsIs an object. Through traversalprops, callingsetPropMethods, for each classpropsSeparate treatment.

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

setPropAccept three parameters:

  • $targetThis is a real DOM object.setPropDOM operations will be performed on this node;
  • nameA that represents the attribute name;
  • valueA that represents the value of the property;

By reading this, I believe you already know about itsetPropWhat needs to be done, under normal circumstances, for ordinaryprops, we will passsetAttributeAttach attributes to DOM objects.

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

However, this is far from enough. Consider the following JSX structure:

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

From the JSX structure above, we find the following points:

  • Due toclassIs a reserved word of JavaScript, commonly used by JSXclassNameTo represent theclass;
  • Generally speakingonThe first attribute represents the event;
  • In addition to character types, attributes can also be boolean values, such asdisabledWhen the value istrueThis attribute is added when the;

So,setPropThe above situation also needs to be considered:

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

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

function setProp($target, name, value) {
    if (name === 'className') { // 因为class是保留字,JSX使用className来表示节点的class
        return $target.setAttribute('class', value);
    } else if (isEventProp(name)) { // 针对 on 开头的属性,为事件
        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);
    }
}

Finally, there is another type of attribute that is our custom attribute, such as state transfer between components in mainstream frameworks, that is, throughpropsTo pass, we don’t want this type of attribute to be displayed in DOM, so we need to write a functionisCustomPropTo check whether this attribute is a custom attribute, because this article is only to implement the core idea of Virtual DOM, for convenience, in this article, this function directly returnsfalse.

function isCustomProp(name) {
    return false;
}

FinalsetPropFunctions:

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);
    }
}

Treatment of child nodes

ForchildrenEvery item in the book is onevNodeObject, when converting the Virtual DOM into the real DOM, the child nodes also need to be recursively converted. it can be imagined that the child nodes need to be recursively called according to the situation that there are child nodestoRealDom, as shown in the following code:

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

Final completiontoRealDomAs follows:

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;
}

Handling changes

The most fundamental reason why Virtual DOM was created is performance improvement. Through Virtual DOM, developers can reduce many unnecessary DOM operations to achieve optimal performance. Then let’s look at how Virtual DOM algorithm achieves performance optimization by comparing the updated Virtual DOM tree with the updated Virtual DOM tree.

Note: This article is the simplest implementation of the author. At present, the algorithm commonly used in the community issnabbdomFor example, Vue is the Virtual DOM implemented by using this algorithm for reference. Interested readers can view the source code of this library. Based on the small example of Virtual DOM in this paper, the author finally also refers to the implementation of this algorithm.Demo portalDue to the limited space, interested readers can study on their own.

To handle changes, first declare aupdateDomFunction that accepts the following four parameters:

  • $parentA that represents the parent node to be mounted;
  • oldVNodeOldVNodeObjects;
  • newVNodeNewVNodeObjects;
  • index, used when updating child nodes, indicating which child node is currently updated, with a default of 0;

The prototype of the function is as follows:

function updateDom($parent, oldVNode, newVNode, index = 0) {

}

Add and delete nodes

First of all, let’s look at the situation of adding a node. For a node that does not exist originally, we need to add a new node to the DOM tree. We need to go throughappendChildTo achieve:

Converted into code expressed as:

// 没有旧的节点,添加新的节点
if (!oldVNode) {
    return $parent.appendChild(toRealDom(newVNode));
}

Similarly, for the case of deleting an old node, we passremoveChildTo implement, here, we should delete the old node from the real DOM, but the problem is that this node is not directly available in this function, we need to know the location of this node in the parent node, in fact, we can pass$parent.childNodes[index]To get, this is why the above mentioned need to be introducedindexA that represents the index of the currently updated node in the parent node:

Converted into code expressed as:

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

// 没有新的节点,删除旧的节点
if (!newVNode) {
    return $parent.removeChild($currentDom);
}

Update node

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

First of all, for text nodes, we can simply handle, for text nodes have changed, only need to compare the old and new string is equal, if it is the same text node, does not need us to update DOM, inupdateDomFunction, directlyreturnJust:

// 都是文本节点,都没有发生变化
if (typeof oldVNode === 'string' && typeof newVNode === 'string' && oldVNode === newVNode) {
    return;
}

Next, consider whether the node really needs updating. As shown in the figure, the type of a node is fromspanChange todiv, obviously, this is definitely need us to updateDOMOf:

We need to write a functionisNodeChangedTo help us judge whether the old node and the new node are really consistent, if not, we need to replace the node:

function isNodeChanged(oldVNode, newVNode) {
    // 一个是textNode,一个是element,一定改变
    if (typeof oldVNode !== typeof newVNode) {
        return true;
    }

    // 都是textNode,比较文本是否改变
    if (typeof oldVNode === 'string' && typeof newVNode === 'string') {
        return oldVNode !== newVNode;
    }

    // 都是element节点,比较节点类型是否改变
    if (typeof oldVNode === 'object' && typeof newVNode === 'object') {
        return oldVNode.type !== newVNode.type;
    }
}

InupdateDomIf the node type is found to have changed, replace the node directly, as shown in the following code, by callingreplaceChildTo remove the old DOM node and add the new DOM node:

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

But this is far from over. Consider the following:

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

Comparing the old and new nodes above, it is found that the node type has not changed, namelyVNode.typeAre all'div'However, the attributes of nodes have changed. In addition to updating DOM for changes in node types, DOM should also be updated for changes in attributes of nodes.

Similar to the above method, we write aisPropsChangedFunction to determine whether the properties of the new and old nodes have changed:

function isPropsChanged(oldProps, newProps) {
    // 类型都不一致,props肯定发生变化了
    if (typeof oldProps !== typeof newProps) {
        return true;
    }

    // props为对象
    if (typeof oldProps === 'object' && typeof newProps === 'object') {
        const oldKeys = Object.keys(oldProps);
        const newkeys = Object.keys(newProps);
        // props的个数都不一样,一定发生了变化
        if (oldKeys.length !== newkeys.length) {
            return true;
        }
        // props的个数相同的情况,遍历props,看是否有不一致的props
        for (let i = 0; i < oldKeys.length; i++) {
            const key = oldKeys[i]
            if (oldProps[key] !== newProps[key]) {
                return true;
            }
        }
        // 默认未改变
        return false;
    }

    return false;
}

Because when a node does not have any attributes,propsFornull,isPropsChangedFirst of all, judge the new and old two nodespropsWhether it is of the same type, that is, whether there are old nodespropsFornull, the new node has new attributes, or vice versa: of the new nodepropsFornull, the properties of the old node were deleted. If the types are inconsistent, then the attribute must be updated.

Next, considering that nodes have both before and after updatingpropsThe situation, we need to judge before and after the updatepropsWhether the two objects are consistent, that is, whether the two objects are congruent, traversal is sufficient. If there are unequal attributes, it is considered thatpropsChange, need to deal withpropsThe change of.

Now, let’s go back to ourupdateDomFunction, look at the Virtual DOM nodepropsThe update of is applied to the real DOM.

// 虚拟DOM的type未改变,对比节点的props是否改变
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
if (isPropsChanged(oldProps, newProps)) {
    const oldPropsKeys = Object.keys(oldProps);
    const newPropsKeys = Object.keys(newProps);

    // 如果新节点没有属性,把旧的节点的属性清除掉
    if (newPropsKeys.length === 0) {
        oldPropsKeys.forEach(propKey => {
            removeProp($currentDom, propKey, oldProps[propKey]);
        });
    } else {
        // 拿到所有的props,以此遍历,增加/删除/修改对应属性
        const allPropsKeys = new Set([...oldPropsKeys, ... newPropsKeys]);
        allPropsKeys.forEach(propKey => {
            // 属性被去除了
            if (!newProps[propKey]) {
                return removeProp($currentDom, propKey, oldProps[propKey]);
            }
            // 属性改变了/增加了
            if (newProps[propKey] !== oldProps[propKey]) {
                return setProp($currentDom, propKey, newProps[propKey]);
            }
        });
    }
}

The above code is also very easy to understand, if foundpropsChanged, then to the oldpropsTo do traversal of each item. Clear the nonexistent attributes and add the newly added attributes to the updated DOM tree:

  • First, if the new node has no attributes, traverse and delete all the attributes of the old node. Here, we callremovePropDeleteremovePropAndsetPropCorrespondingly, due to the limited space in 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);
    }
}
  • If the new node has attributes, all attributes of the old node and the new node are obtained, and all attributes of the old node and the new node are traversed; if the attributes do not exist in the new node, then the attributes are deleted. If the new node is inconsistent with the old node attribute or is a newly added attribute, callsetPropAdd new attributes to real DOM nodes.

Update child node

In the end, withtoRealDomSimilarly, inupdateDomIn, we should also handle all child nodes and recursively call themupdateDomA that compares all child nodes one by oneVNodeIs there any update, onceVNodeIf there is an update, the real DOM also needs to be re-rendered:

// 根节点相同,但子节点不同,要递归对比子节点
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);
    }
}

It is far from over.

The above is the simplest Virtual DOM code implemented by the author, but it is quite different from the Virtual DOM algorithm we use in the community. Here is the simplest example:

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

For the implementation in the above codeupdateDomFunction, the DOM structure before and after the update is shown above, which triggers fiveliAll nodes are re-rendered, which is obviously a waste of performance. AndsnabbdomThe above problems are well solved by the way of mobile nodes. Due to the limited space of this article and the community’s many analysis articles on the Virtual DOM algorithm, the author will not elaborate too much in this article, and interested readers can study it by themselves. The author also based on this example, referencesnabbdomThe final version of the algorithm is implemented, and interested readers can view the examples in this article.Final version