了解react、vue的一大核心技术:虚拟DOM的实现原理

实现一个简单的虚拟DOM

如今的流行框架,不管React仍是Vue,都采用虚拟DOM。好处就是,当咱们数据变化时,无需像Backbone那样总体从新渲染,而是局部刷新变化部分,以下组件模版:bash

<ul class="list">
    <li>item1</li>
    <li>item2</li>
</ul>复制代码

当页面中item2变为item3时,如Backbone同样的MVC框架就会将ul这个模块总体刷新,而若是咱们采用虚拟DOM来实现,就会只将'item2'这个文本节点变为'item3'文本节点。数据结构

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

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

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

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

1、构建虚拟DOM
this

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

经过JavaScript,咱们能够很容易构建它,以下:
prototype

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构造函数,以下:code

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到父节点中

好了,那咱们采用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());复制代码

2、处理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的实现原理了。

相关文章
相关标签/搜索