【Vue】實現一個簡單的虛擬DOM


現在的流行框架,無論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>

 

轉自:https://www.cnblogs.com/giggle/p/7538533.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM