180

实现一个简单的虚拟DOM - 猴子猿

 6 years ago
source link: http://www.cnblogs.com/giggle/p/7538533.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

实现一个简单的虚拟DOM 全屏模式

2017-09-22 13:02  猴子猿  阅读(5946)  评论(7)  编辑  收藏  举报

现在的流行框架,无论React还是Vue,都采用虚拟DOM。

好处就是,当我们数据变化时,无需像Backbone那样整体重新渲染,而是局部刷新变化部分,如下组件模版:

<ul class="list">
    <li>item1</li>
    <li>item2</li>
</ul>

当页面中item2变为item3时,如Backbone一样的MVC框架就会将ul这个模块整体刷新,而如果我们采用虚拟DOM来实现,就会只将'item2'这个文本节点变为'item3'文本节点。

初看虚拟DOM,感觉很玄乎,但是剥开它华丽的外衣,也就那样:

1. 通过JavaScript来构建虚拟的DOM树结构,并将其呈现到页面中;

2. 当数据改变,引起DOM树结构发生改变,从而生成一颗新的虚拟DOM树,将其与之前的DOM对比,将变化部分应用到真实的DOM树中,即页面中。 

通过上面的介绍,下面,我们就来实现一个简单的虚拟DOM,并将其与真实的DOM关联。

一、构建虚拟DOM

虚拟DOM,其实就是用JavaScript对象来构建DOM树,如上ul组件模版,其树形结构如下:

通过JavaScript,我们可以很容易构建它,如下:

var elem = Element({
                tagName: 'ul',
                props: {'class': 'list'},
                children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['item2']})
                ]
            });

note:Element为一个构造函数,返回一个Element对象。为了更清晰的呈现虚拟DOM结构,我们省略了new,而在Element中实现。

看了上面JavaScript构建的虚拟DOM树,不难实现Element构造函数,如下:

/*
* @Params:
*     tagName(string)(requered)
*     props(object)(optional)
*     children(array)(optional)
* */
function Element({tagName, props, children}){
    if(!(this instanceof Element)){
        return new Element({tagName, props, children})
    }
    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
}

好了,通过Element我们可以任意地构建虚拟DOM树了。但是有个问题,虚拟的终归是虚拟的,我们得将其呈现到页面中,不然,没卵用。。

怎么呈现呢?

从上面得知,这是一颗树嘛,那我们就通过遍历,逐个节点地创建真实DOM节点:

  1. createElement;

  2. createTextNode.

怎么遍历呢?

因为这是一颗树嘛,对于树形结构无外乎两种遍历:

  1. 深度优先遍历(DFS)

  2. 广度优先遍历(BFS)

下面我们就来回顾下《数据结构》中,这两种遍历的思想:

1. DFS利用栈来遍历数据,如下:

2. BFS利用队列来遍历数据,如下:

针对实际情况,我们得采用DFS,为什么呢?

那尼,还是这种疑问?!!因为我们得将子节点append到父节点中,如果采用BFS搞毛线啊!!

好了,那我们采用DFS,就来实现一个render函数吧,如下:

Element.prototype.render = function(){
    var el = document.createElement(this.tagName),
        props = this.props,
        propName,
        propValue;
    for(propName in props){
        propValue = props[propName];
        el.setAttribute(propName, propValue);
    }
    this.children.forEach(function(child){
        var childEl = null;
        if(child instanceof Element){
            childEl = child.render();
        }else{
            childEl = document.createTextNode(child);
        }
        el.appendChild(childEl);
    });
    return el;
};

此时,我们就可以轻松地将虚拟DOM呈现到指定真实DOM中啦。假设,我们将上诉ul虚拟DOM呈现到页面body中,如下:

var elem = Element({
                tagName: 'ul',
                props: {'class': 'list'},
                children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['item2']})
                ]
            });
document.querySelector('body').appendChild(elem.render());
二、处理DOM更新

在前一小结,我们成功地实现了虚拟DOM,并将其转化为真实DOM,呈现在页面中。

接下来,我们就处理当DOM更新时,怎样通过新旧虚拟DOM对比,然后将变化部分更新到真实DOM中的问题。

DOM更新,无外乎四种情况,如下:

  1. 新增节点;

  2. 删除节点;

  3. 替换节点;

  4. 父节点相同,对比子节点.

好了,需求了解,开始我们的表演。

毫无疑问,遍历DOM树仍然采用DFS遍历。

因为我们要将变化的节点更新到真实DOM中,所以还得传入真实的DOM根节点,并且真实的DOM节点与虚拟的DOM节点,树形结构一致,故通过标记可以记录节点变化位置,如下:

实现函数如下:

function updateElement($root, newElem, oldElem, index = 0) {
    if (!oldElem){
        $root.appendChild(newElem.render());
    } else if (!newElem) {
        $root.removeChild($root.childNodes[index]);
    } else if (changed(newElem, oldElem)) {
        if (typeof newElem === 'string') {
            $root.childNodes[index].textContent = newElem;
        } else {
            $root.replaceChild(newElem.render(), $root.childNodes[index]);
        }
    } else if (newElem.tagName) {
        let newLen = newElem.children.length;
        let oldLen = oldElem.children.length;
        for (let i = 0; i < newLen || i < oldLen; i++) {
            updateElement($root.childNodes[index], newElem.children[i], oldElem.children[i], i)
        }
    }
}

其中的changed方法,简单实现如下:

function changed(elem1, elem2) {
    return (typeof elem1 !== typeof elem2) ||
           (typeof elem1 === 'string' && elem1 !== elem2) ||
           (elem1.type !== elem2.type);
}

好了,一个简单的虚拟DOM就实现了。

三、效果展示

通过JS构建一颗虚拟DOM(如上诉ul),并将其呈现到页面中,然后替换其子节点,动态更新到真实DOM中,如下:

<body>
        <button id="refresh">refresh element</button>
        <div id="root"></div>
        <script src="./virtualDom.js"></script>
        <script>
            var elem = Element({
                tagName: 'ul',
                props: {'class': 'list'},
                children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['item2']})
                ]
            });
            var newElem =  Element({
                tagName: 'ul',
                props: {'class': 'list'},
                children: [
                    Element({tagName: 'li', children: ['item1']}),
                    Element({tagName: 'li', children: ['hahaha']})
                ]
            });
            var $root = document.querySelector('#root');
            var $refresh = document.querySelector('#refresh');
            updateElement($root, elem);
            $refresh.addEventListener('click', () => {
                updateElement($root, newElem, elem);
            });
        </script>
    </body>

效果如下:

四、拓展阅读

[1]. 树结构之JavaScript

[2]. virtual dom

[3]. 虚拟DOM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK