如今的流行框架,不管React仍是Vue,都採用虛擬DOM。html
好處就是,當咱們數據變化時,無需像Backbone那樣總體從新渲染,而是局部刷新變化部分,以下組件模版:git
<ul class="list"> <li>item1</li> <li>item2</li> </ul>
當頁面中item2變爲item3時,如Backbone同樣的MVC框架就會將ul這個模塊總體刷新,而若是咱們採用虛擬DOM來實現,就會只將'item2'這個文本節點變爲'item3'文本節點。github
初看虛擬DOM,感受很玄乎,可是剝開它華麗的外衣,也就那樣:數據結構
1. 經過JavaScript來構建虛擬的DOM樹結構,並將其呈現到頁面中;app
2. 當數據改變,引發DOM樹結構發生改變,從而生成一顆新的虛擬DOM樹,將其與以前的DOM對比,將變化部分應用到真實的DOM樹中,即頁面中。 框架
經過上面的介紹,下面,咱們就來實現一個簡單的虛擬DOM,並將其與真實的DOM關聯。dom
1、構建虛擬DOM |
虛擬DOM,其實就是用JavaScript對象來構建DOM樹,如上ul組件模版,其樹形結構以下:函數
經過JavaScript,咱們能夠很容易構建它,以下:this
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中實現。spa
看了上面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());
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就實現了。
3、效果展現 |
經過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>
效果以下:
4、拓展閱讀 |
[1]. 樹結構之JavaScript
[2]. virtual dom
[3]. 虛擬DOM