实现一个简单的虚拟DOM

来源:互联网 发布:golang 事件总线 编辑:程序博客网 时间:2024/05/01 20:42

原文地址:http://www.cnblogs.com/giggle/p/7538533.html

现在的流行框架,无论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

原创粉丝点击