實現一個簡單的虛擬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']})
]
});複製程式碼
看了上面JavaScript構建的虛擬DOM樹,不難實現Element建構函式,如下:
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節點:
怎麼遍歷呢?
因為這是一顆樹嘛,對於樹形結構無外乎兩種遍歷:
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());複製程式碼
二、處理DOM更新
在前一小結,我們成功地實現了虛擬DOM,並將其轉化為真實DOM,呈現在頁面中。
接下來,我們就處理當DOM更新時,怎樣通過新舊虛擬DOM對比,然後將變化部分更新到真實DOM中的問題。
DOM更新,無外乎四種情況,如下:
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的實現原理了。