如今的流行框架,不管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']})
]
});複製代碼
看了上面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節點:
怎麼遍歷呢?
由於這是一顆樹嘛,對於樹形結構無外乎兩種遍歷:
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更新,無外乎四種狀況,以下:
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的實現原理了。