Why use the Virtual DOM

  • Manipulating the DOM manually is cumbersome. Browser compatibility issues also need to be considered. Although libraries such as JQuery simplify DOM manipulation, the complexity of DOM manipulation increases with the complexity of projects.

  • In order to simplify the complex manipulation of DOM, various MVVM frameworks have emerged to solve the problem of view and state synchronization

  • To simplify view manipulation we could use a template engine, but the problem of tracking state changes was not solved by the template engine, so the Virtual DOM emerged

  • The advantage of the Virtual DOM is that there is no need to update the DOM immediately when the state changes. You just need to create a Virtual tree to describe the DOM, and the Virtual DOM itself will figure out how to update the DOM effectively (using the Diff algorithm).

Features of the Virtual DOM

  1. The Virtual DOM maintains the state of the program, keeping track of the last state.
  2. Update the real DOM by comparing the two states.

Implement a basic Virtual DOM library

We can echo snabbdom library https://github.com/snabbdom/snabbdom.git diy achieve a mini version of the Virtual DOM library.

First, we create an index.html file and write what we want to display. It looks like this:

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>vdom</title>
    <style>
        .main {
            color: #00008b;
        }
        .main1{
            font-weight: bold;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="./vdom.js"></script>
    <script>
        function render() {
            return h('div', {
                style: useObjStr({
                    'color': '#ccc'.'font-size': '20px'
                })
            }, [
                h('div', {}, [h('span', {
                    onClick: () = > {
                        alert('1'); }},'text'), h('a', {
                    href: 'https://www.baidu.com'.class: 'main main1'
                }, 'click')]),])}// The page changes
        function render1() {
            return h('div', {
                style: useStyleStr({
                    'color': '#ccc'.'font-size': '20px'
                })
            }, [
                h('div', {}, [h('span', {
                    onClick: () = > {
                        alert('1'); }},'The text has changed')]),])}// First load
        mountNode(render, '#app');

        // State changes
        setTimeout(() = >{
            mountNode(render1, '#app');
        },3000)
    </script>
</body>

</html>
Copy the code

Inside the body tag we create a DOM element of the app with an ID for the mounted node. Next we introduce a vdom.js file, which is the mini-version of the Virtual DOM library we will implement. Finally, we define a Render method inside the script tag, which returns an H method. Call the mountNode method to mount it to the DOM element whose ID is app. In the data structure of h method, we refer to the SNabbDOM library. The first parameter is the label name, the second parameter is the attribute, and the last parameter is the child node. Also, you may have noticed that in the H method we use the useStyleStr method, which is used to convert the style style into a structure that the page can recognize. The code will be shown at the end.

The idea is clear, and the code for the display page is finished. Now we’ll focus on vdom.js and how to implement it step by step.

The first step

We see that the index. HTML file first needs to call the mountNode method, so we define a mountNode method in the vdom.js file.

// Mount node
function mountNode(render, selector) {}Copy the code

We then see that the first argument to the mountNode method is the Render method, which returns the H method, and that the first argument is a label, the second argument is a property, and the third argument is a child node.

So, let’s define another h method in the vdom.js file.

 function h(tag, props, children) {
    return { tag, props, children };
}
Copy the code

We’re not done yet, we need to mount the page based on the three parameters tag, props, and children passed in.

We need to do this. We wrap a mount method inside the mountNode method and pass the parameters passed to the mountNode method to the mount method.

// Mount node
function mountNode(render, selector) {
  mount(render(), document.querySelector(selector))
}
Copy the code

Next, we define a mount method.

function mount(vnode, container) {
    const el = document.createElement(vnode.tag);
    vnode.el = el;
    // props
    if (vnode.props) {
        for (const key in vnode.props) {
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                    passive:true})}else{ el.setAttribute(key, vnode.props[key]); }}}if (vnode.children) {
        if (typeof vnode.children === "string") {
            el.textContent = vnode.children;
        } else {
            vnode.children.forEach(child= > {
                mount(child, el);
            });
        }
    }
    
    container.appendChild(el);
}
Copy the code

The first argument is to call the passed render method, which returns h, and h returns an object with the same name as {tag, props, children}, so we can call vnode.tag, vnode.props, and vnode.children.

If the property field starts with the on flag, that is, the event, then we create a listener event from the third bit of the property field using the addEventListenerAPI. Otherwise, set the attribute directly using the setAttributeAPI.

Next, we determine the child node. If it is a string, we assign the string directly to the text node. Otherwise it’s a node and we recursively call the mount method.

Finally, we’ll use the appendChildAPI to mount the node content into the real DOM.

The page is displayed properly.

The second step

We know that Virtual DOM has the following two features:

  1. The Virtual DOM maintains the state of the program, keeping track of the last state.
  2. Update the real DOM by comparing the two states.

So this takes advantage of the diff algorithm that we talked about earlier.

We first define a patch method. The first parameter is the old node, and the second parameter is the new node, because you want to compare the states before and after.

function patch(n1, n2) {}Copy the code

Now, we need to do one more thing, that is to improve the mountNode method, why do this? This is because when the state changes, only the DOM with the changed state is updated, which is what we call differential updating. In this case, the PATCH method is needed to do the DIFF algorithm.

Compared with the previous, we added the judgment on whether to mount the node. If not, the node is mounted directly by calling the mount method. Otherwise, the patch method is called for difference update.

let isMounted = false;
let oldTree;

// Mount node
function mountNode(render, selector) {
    if(! isMounted) { mount(oldTree = render(),document.querySelector(selector));
        isMounted = true;
    } else {
        constnewTree = render(); patch(oldTree, newTree); oldTree = newTree; }}Copy the code

Then we will take a look at the patch method, which is the most complex method in this library.

function patch(n1, n2) {
    // Implement this
    // 1. check if n1 and n2 are of the same type
    if(n1.tag ! == n2.tag) {// 2. if not, replace
        const parent = n1.el.parentNode;
        const anchor = n1.el.nextSibling;
        parent.removeChild(n1.el);
        mount(n2, parent, anchor);
        return
    }

    const el = n2.el = n1.el;

    // 3. if yes
    / / 3.1 diff props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    for (const key in newProps) {
        const newValue = newProps[key];
        const oldValue = oldProps[key];
        if(newValue ! == oldValue) {if(newValue ! =null) {
                el.setAttribute(key, newValue);
            } else{ el.removeAttribute(key); }}}for (const key in oldProps) {
        if(! (keyinnewProps)) { el.removeAttribute(key); }}/ / diff 3.2 children
    const oc = n1.children;
    const nc = n2.children;
    if (typeof nc === 'string') {
        if (nc !== oc) {
            el.textContent = nc;
        }
    } else if (Array.isArray(nc)) {
        if (Array.isArray(oc)) {
            // array diff
            const commonLength = Math.min(oc.length, nc.length);
            for (let i = 0; i < commonLength; i++) {
                patch(oc[i], nc[i]);
            }
            if (nc.length > oc.length) {
                nc.slice(oc.length).forEach(c= > mount(c, el));
            } else if (oc.length > nc.length) {
                oc.slice(nc.length).forEach(c= >{ el.removeChild(c.el); }}})else {
            el.innerHTML = ' ';
            nc.forEach(c= >mount(c, el)); }}}Copy the code

The two parameters are oldTree and newTree passed in the mountNode method respectively. First, we compare the labels of the old and new nodes.

If the labels of the old and new nodes are not equal, remove the old node. Also, use the nextSiblingAPI to take the node immediately after the specified node (in the same tree hierarchy). Then, pass the third argument to the mount method. The mount method takes two parameters, you might wonder. Yes, but here we need to pass in the third parameter, mainly for the sibling nodes.

  if(n1.tag ! == n2.tag) {// 2. if not, replace
        const parent = n1.el.parentNode;
        const anchor = n1.el.nextSibling;
        parent.removeChild(n1.el);
        mount(n2, parent, anchor);
        return
    }
Copy the code

So, let’s modify the mount method. We see that we just added a judgment on whether the Anchor parameter is empty.

If the anchor parameter is not empty, we use insertBeforeAPI to insert a child node with the specified parent before the reference node. InsertBeforeAPI the first parameter is the node to be inserted, the second parameter is to be inserted before the node, and if this parameter is null the node to be inserted is inserted at the end of the child node.

If the anchor parameter is empty, add the child node directly to the end of the child node list under the parent node.

function mount(vnode, container, anchor) {
    const el = document.createElement(vnode.tag);
    vnode.el = el;
    // props
    if (vnode.props) {
        for (const key in vnode.props) {
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                    passive:true})}else{ el.setAttribute(key, vnode.props[key]); }}}if (vnode.children) {
        if (typeof vnode.children === "string") {
            el.textContent = vnode.children;
        } else {
            vnode.children.forEach(child= >{ mount(child, el); }); }}if (anchor) {
        container.insertBefore(el, anchor);
    } else{ container.appendChild(el); }}Copy the code

Next, we return to the patch method. If the labels of the old and new nodes are equal, we first iterate over the attributes of the old and new nodes. We first traverse the attributes of the new node to determine whether the values of the attributes of the old and new nodes are the same. If they are not, we will proceed with further processing. Check whether the attribute value of the new node is null; otherwise, remove the attribute directly. It then iterates through the properties of the old node and removes the properties directly if the property name is not in the new node property sheet.

After analyzing the comparison of the old and new node attributes, let’s analyze the third parameter child node.

Firstly, we define two variables oc and NC respectively, and assign the children attribute of the old node and the children attribute of the new node respectively. If the children property of the new node is a string and the contents of the old and new nodes are different, simply assign the text content of the new node.

Next, we see that the array.isarray () method is used to determine whether the children property of the new node is an Array, and if so, execute the following code.

else if (Array.isArray(nc)) {
        if (Array.isArray(oc)) {
            // array diff
            const commonLength = Math.min(oc.length, nc.length);
            for (let i = 0; i < commonLength; i++) {
                patch(oc[i], nc[i]);
            }
            if (nc.length > oc.length) {
                nc.slice(oc.length).forEach(c= > mount(c, el));
            } else if (oc.length > nc.length) {
                oc.slice(nc.length).forEach(c= >{ el.removeChild(c.el); }}})else {
            el.innerHTML = ' ';
            nc.forEach(c= >mount(c, el)); }}Copy the code

The children property of the old node is an array.

If so, we take the minimum of both the length of the new and the length of the child node array. Then, we recurse it to the patch method. Why take the minimum? Because if I take the lengths that they share. Then, each time the recursion is traversed, the size of nc.length and oc.length is judged and the corresponding method is iterated.

If not, clear the node content and repeat the mount method.

This completes our mini-version of the Virtual DOM library.

The following page is displayed.

The source code

index.html

<! DOCTYPEhtml>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>vdom</title>
    <style>
        .main {
            color: #00008b;
        }
        .main1{
            font-weight: bold;
        }
    </style>
</head>

<body>
    <div id="app"></div>
    <script src="./vdom.js"></script>
    <script>
        function render() {
            return h('div', {
                style: useObjStr({
                    'color': '#ccc'.'font-size': '20px'
                })
            }, [
                h('div', {}, [h('span', {
                    onClick: () = > {
                        alert('1'); }},'text'), h('a', {
                    href: 'https://www.baidu.com'.class: 'main main1'
                }, 'click')]),])}// The page changes
        function render1() {
            return h('div', {
                style: useStyleStr({
                    'color': '#ccc'.'font-size': '20px'
                })
            }, [
                h('div', {}, [h('span', {
                    onClick: () = > {
                        alert('1'); }},'The text has changed')]),])}// First load
        mountNode(render, '#app');

        // State changes
        setTimeout(() = >{
            mountNode(render1, '#app');
        },3000)
    </script>
</body>

</html>
Copy the code

vdom.js

 // vdom ---
 function h(tag, props, children) {
    return { tag, props, children };
}

function mount(vnode, container, anchor) {
    const el = document.createElement(vnode.tag);
    vnode.el = el;
    // props
    if (vnode.props) {
        for (const key in vnode.props) {
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key],{
                    passive:true})}else{ el.setAttribute(key, vnode.props[key]); }}}if (vnode.children) {
        if (typeof vnode.children === "string") {
            el.textContent = vnode.children;
        } else {
            vnode.children.forEach(child= >{ mount(child, el); }); }}if (anchor) {
        container.insertBefore(el, anchor);
    } else{ container.appendChild(el); }}// processing strings
function useStyleStr(obj) {
    const reg = /^{|}/g;
    const reg1 = new RegExp('"'."g");
    const str = JSON.stringify(obj);
    const ustr = str.replace(reg, ' ').replace(', '.'; ').replace(reg1,' ');
    return ustr;
}

function patch(n1, n2) {
    // Implement this
    // 1. check if n1 and n2 are of the same type
    if(n1.tag ! == n2.tag) {// 2. if not, replace
        const parent = n1.el.parentNode;
        const anchor = n1.el.nextSibling;
        parent.removeChild(n1.el);
        mount(n2, parent, anchor);
        return
    }

    const el = n2.el = n1.el;

    // 3. if yes
    / / 3.1 diff props
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    for (const key in newProps) {
        const newValue = newProps[key];
        const oldValue = oldProps[key];
        if(newValue ! == oldValue) {if(newValue ! =null) {
                el.setAttribute(key, newValue);
            } else{ el.removeAttribute(key); }}}for (const key in oldProps) {
        if(! (keyinnewProps)) { el.removeAttribute(key); }}/ / diff 3.2 children
    const oc = n1.children;
    const nc = n2.children;
    if (typeof nc === 'string') {
        if (nc !== oc) {
            el.textContent = nc;
        }
    } else if (Array.isArray(nc)) {
        if (Array.isArray(oc)) {
            // array diff
            const commonLength = Math.min(oc.length, nc.length);
            for (let i = 0; i < commonLength; i++) {
                patch(oc[i], nc[i]);
            }
            if (nc.length > oc.length) {
                nc.slice(oc.length).forEach(c= > mount(c, el));
            } else if (oc.length > nc.length) {
                oc.slice(nc.length).forEach(c= >{ el.removeChild(c.el); }}})else {
            el.innerHTML = ' ';
            nc.forEach(c= >mount(c, el)); }}}let isMounted = false;
let oldTree;

// Mount node
function mountNode(render, selector) {
    if(! isMounted) { mount(oldTree = render(),document.querySelector(selector));
        isMounted = true;
    } else {
        constnewTree = render(); patch(oldTree, newTree); oldTree = newTree; }}Copy the code

About the author

Author: Vam’s Golden Bean Road. CSDN blog star of the Year 2019, CSDN blog has reached millions of visitors. Nuggets blog post repeatedly pushed to the home page, the total page view has reached hundreds of thousands.

In addition, my public number: front-end experience robbed road, the public continues to update the latest front-end technology and related technical articles. Welcome to pay attention to my public number, let us together in front of the road experience rob it! Go!