DOM是很慢的,其元素很是龐大,頁面的性能問題鮮有由JS引發的,大部分都是由DOM操做引發的。若是對前端工做進行抽象的話,主要就是維護狀態和更新視圖;而更新視圖和維護狀態都須要DOM操做。其實近年來,前端的框架主要發展方向就是解放DOM操做的複雜性。javascript
在jQuery出現之前,咱們直接操做DOM結構,這種方法複雜度高,兼容性也較差;有了jQuery強大的選擇器以及高度封裝的API,咱們能夠更方便的操做DOM,jQuery幫咱們處理兼容性問題,同時也使DOM操做變得簡單;可是聰明的程序員不可能知足於此,各類MVVM框架應運而生,有angularJS、avalon、vue.js等,MVVM使用數據雙向綁定,使得咱們徹底不須要操做DOM了,更新了狀態視圖會自動更新,更新了視圖數據狀態也會自動更新,能夠說MMVM使得前端的開發效率大幅提高,可是其大量的事件綁定使得其在複雜場景下的執行性能堪憂;有沒有一種兼顧開發效率和執行效率的方案呢?ReactJS就是一種不錯的方案,雖然其將JS代碼和HTML代碼混合在一塊兒的設計有很多爭議,可是其引入的Virtual DOM(虛擬DOM)倒是獲得你們的一致認同的。前端
虛擬的DOM的核心思想是:對複雜的文檔DOM結構,提供一種方便的工具,進行最小化地DOM操做。這句話,也許過於抽象,卻基本概況了虛擬DOM的設計思想vue
(1) 提供一種方便的工具,使得開發效率獲得保證 (2) 保證最小化的DOM操做,使得執行效率獲得保證
DOM很慢,而javascript很快,用javascript對象能夠很容易地表示DOM節點。DOM節點包括標籤、屬性和子節點,經過VElement表示以下。java
//虛擬dom,參數分別爲標籤名、屬性對象、子DOM列表 var VElement = function(tagName, props, children) { //保證只能經過以下方式調用:new VElement if (!(this instanceof VElement)) { return new VElement(tagName, props, children); } //能夠經過只傳遞tagName和children參數 if (util.isArray(props)) { children = props; props = {}; } //設置虛擬dom的相關屬性 this.tagName = tagName; this.props = props || {}; this.children = children || []; this.key = props ? props.key : void 666; var count = 0; util.each(this.children, function(child, i) { if (child instanceof VElement) { count += child.count; } else { children[i] = '' + child; } count++; }); this.count = count; }
經過VElement,咱們能夠很簡單地用javascript表示DOM結構。好比node
var vdom = velement('div', { 'id': 'container' }, [ velement('h1', { style: 'color:red' }, ['simple virtual dom']), velement('p', ['hello world']), velement('ul', [velement('li', ['item #1']), velement('li', ['item #2'])]), ]);
上面的javascript代碼能夠表示以下DOM結構:程序員
<div id="container"> <h1 style="color:red">simple virtual dom</h1> <p>hello world</p> <ul> <li>item #1</li> <li>item #2</li> </ul> </div>
一樣咱們能夠很方便地根據虛擬DOM樹構建出真實的DOM樹。具體思路:根據虛擬DOM節點的屬性和子節點遞歸地構建出真實的DOM樹。見以下代碼:算法
VElement.prototype.render = function() { //建立標籤 var el = document.createElement(this.tagName); //設置標籤的屬性 var props = this.props; for (var propName in props) { var propValue = props[propName] util.setAttr(el, propName, propValue); } //依次建立子節點的標籤 util.each(this.children, function(child) { //若是子節點仍然爲velement,則遞歸的建立子節點,不然直接建立文本類型節點 var childEl = (child instanceof VElement) ? child.render() : document.createTextNode(child); el.appendChild(childEl); }); return el; }
對一個虛擬的DOM對象VElement,調用其原型的render方法,就能夠產生一顆真實的DOM樹。數據結構
vdom.render();
既然咱們能夠用JS對象表示DOM結構,那麼當數據狀態發生變化而須要改變DOM結構時,咱們先經過JS對象表示的虛擬DOM計算出實際DOM須要作的最小變更,而後再操做實際DOM,從而避免了粗放式的DOM操做帶來的性能問題。app
在用JS對象表示DOM結構後,當頁面狀態發生變化而須要操做DOM時,咱們能夠先經過虛擬DOM計算出對真實DOM的最小修改量,而後再修改真實DOM結構(由於真實DOM的操做代價太大)。框架
以下圖所示,兩個虛擬DOM之間的差別已經標紅:
爲了便於說明問題,我固然選取了最簡單的DOM結構,兩個簡單DOM之間的差別彷佛是顯而易見的,可是真實場景下的DOM結構很複雜,咱們必須藉助於一個有效的DOM樹比較算法。
設計一個diff算法有兩個要點:
如何比較兩個兩棵DOM樹 如何記錄節點之間的差別
<1> 如何比較兩個兩棵DOM樹
計算兩棵樹之間差別的常規算法複雜度爲O(n3),一個文檔的DOM結構有上百個節點是很正常的狀況,這種複雜度沒法應用於實際項目。針對前端的具體狀況:咱們不多跨級別的修改DOM節點,一般是修改節點的屬性、調整子節點的順序、添加子節點等。所以,咱們只須要對同級別節點進行比較,避免了diff算法的複雜性。對同級別節點進行比較的經常使用方法是深度優先遍歷:
function diff(oldTree, newTree) { //節點的遍歷順序 var index = 0; //在遍歷過程當中記錄節點的差別 var patches = {}; //深度優先遍歷兩棵樹 dfsWalk(oldTree, newTree, index, patches); return patches; }
<2>如何記錄節點之間的差別
因爲咱們對DOM樹採起的是同級比較,所以節點之間的差別能夠歸結爲4種類型:
修改節點屬性, 用PROPS表示 修改節點文本內容, 用TEXT表示 替換原有節點, 用REPLACE表示 調整子節點,包括移動、刪除等,用REORDER表示
對於節點之間的差別,咱們能夠很方便地使用上述四種方式進行記錄,好比當舊節點被替換時:
{type:REPLACE,node:newNode}
而當舊節點的屬性被修改時:
{type:PROPS,props: newProps}
在深度優先遍歷的過程當中,每一個節點都有一個編號,若是對應的節點有變化,只須要把相應變化的類別記錄下來便可。下面是具體實現:
function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = []; if (newNode === null) { //依賴listdiff算法進行標記爲刪除 } else if (util.isString(oldNode) && util.isString(newNode)) { if (oldNode !== newNode) { //若是是文本節點則直接替換文本 currentPatch.push({ type: patch.TEXT, content: newNode }); } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { //節點類型相同 //比較節點的屬性是否相同 var propsPatches = diffProps(oldNode, newNode); if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }); } //比較子節點是否相同 diffChildren(oldNode.children, newNode.children, index, patches, currentPatch); } else { //節點的類型不一樣,直接替換 currentPatch.push({ type: patch.REPLACE, node: newNode }); } if (currentPatch.length) { patches[index] = currentPatch; } }
好比對上文圖中的兩顆虛擬DOM樹,能夠用以下數據結構記錄它們之間的變化:
var patches = { 1:{type:REPLACE,node:newNode}, //h1節點變成h5 5:{type:REORDER,moves:changObj} //ul新增了子節點li }
經過虛擬DOM計算出兩顆真實DOM樹之間的差別後,咱們就能夠修改真實的DOM結構了。上文深度優先遍歷過程產生了用於記錄兩棵樹之間差別的數據結構patches, 經過使用patches咱們能夠方便對真實DOM作最小化的修改。
//將差別應用到真實DOM function applyPatches(node, currentPatches) { util.each(currentPatches, function(currentPatch) { switch (currentPatch.type) { //當修改類型爲REPLACE時 case REPLACE: var newNode = (typeof currentPatch.node === 'String') ? document.createTextNode(currentPatch.node) : currentPatch.node.render(); node.parentNode.replaceChild(newNode, node); break; //當修改類型爲REORDER時 case REORDER: reoderChildren(node, currentPatch.moves); break; //當修改類型爲PROPS時 case PROPS: setProps(node, currentPatch.props); break; //當修改類型爲TEXT時 case TEXT: if (node.textContent) { node.textContent = currentPatch.content; } else { node.nodeValue = currentPatch.content; } break; default: throw new Error('Unknow patch type ' + currentPatch.type); } }); }
原文來自於:http://blog.csdn.net/yczz/article/details/51292169