全面理解虛擬DOM,實現虛擬DOM
最近一兩年前端最火的技術莫過於ReactJS,即便你沒用過也該聽過,ReactJS由業界頂尖的網際網路公司facebook提出,其本身有很多先進的設計思路,比如頁面UI元件化、虛擬DOM等。本文將帶你解開虛擬DOM的神祕面紗,不僅要理解其原理,而且要實現一個基本可用的虛擬DOM。
1.為什麼需要虛擬DOM
DOM是很慢的,其元素非常龐大,頁面的效能問題鮮有由JS引起的,大部分都是由DOM操作引起的。如果對前端工作進行抽象的話,主要就是維護狀態和更新檢視;而更新檢視和維護狀態都需要DOM操作。其實近年來,前端的框架主要發展方向就是解放DOM操作的複雜性。
在jQuery出現以前,我們直接操作DOM結構,這種方法複雜度高,相容性也較差;有了jQuery強大的選擇器以及高度封裝的API,我們可以更方便的操作DOM,jQuery幫我們處理相容性問題,同時也使DOM操作變得簡單;但是聰明的程式設計師不可能滿足於此,各種MVVM框架應運而生,有angularJS、avalon、vue.js等,MVVM使用資料雙向繫結,使得我們完全不需要操作DOM了,更新了狀態檢視會自動更新,更新了檢視資料狀態也會自動更新,可以說MMVM使得前端的開發效率大幅提升,但是其大量的事件繫結使得其在複雜場景下的執行效能堪憂;有沒有一種兼顧開發效率和執行效率的方案呢?ReactJS就是一種不錯的方案,雖然其將JS程式碼和HTML程式碼混合在一起的設計有不少爭議,但是其引入的Virtual DOM(虛擬DOM)卻是得到大家的一致認同的。
2.理解虛擬DOM
虛擬的DOM的核心思想是:對複雜的文件DOM結構,提供一種方便的工具,進行最小化地DOM操作。這句話,也許過於抽象,卻基本概況了虛擬DOM的設計思想
(1) 提供一種方便的工具,使得開發效率得到保證
(2) 保證最小化的DOM操作,使得執行效率得到保證
(1).用JS表示DOM結構
DOM很慢,而javascript很快,用javascript物件可以很容易地表示DOM節點。DOM節點包括標籤、屬性和子節點,通過VElement表示如下。
//虛擬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結構。比如
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操作帶來的效能問題。
(2).比較兩棵虛擬DOM樹的差異
在用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
}
(3).對真實DOM進行最小化修改
通過虛擬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);
}
});
}
至此,虛擬DOM的基本原理已經基本講解完成了;我們也一起實現了一個基本可用的虛擬DOM。本文中只給出了關鍵的原始碼,全部原始碼請參考我的github。
本文同時發表在我的部落格積木村の研究所 :http://foio.github.io/virtual-dom/
相關文章
- React 虛擬Dom 轉成 真實Dom 實現原理React
- REACT——虛擬DOMReact
- 初探虛擬 DOM
- Vue虛擬DOMVue
- vue虛擬dom原理Vue
- 實現一個簡單的虛擬DOM
- 虛擬Dom與Diff的簡單實現
- React 的虛擬 DOM 和 Vue 的虛擬 DOM 有什麼區別?ReactVue
- 虛擬Dom詳解 - (一)
- [譯] 認識虛擬 DOM
- Vue 為什麼要用虛擬 DOM(Virtual DOM)Vue
- 虛擬dom優化效能的表現優化
- 通過編寫簡易虛擬DOM,來學習虛擬DOM 的知識!
- React虛擬DOM的好處React
- Vitural Dom & diff演算法 (一) 虛擬碼實現演算法
- 虛擬DOM與diff演算法演算法
- vue 快速入門 系列 —— 虛擬 DOMVue
- vue2.0的虛擬DOM渲染Vue
- React虛擬Dom渲染演算法React演算法
- VirtualDOM----snabbdom虛擬dom庫 demo
- 閱讀vue原始碼後,簡單實現虛擬domVue原始碼
- 基於虛擬DOM(Snabbdom)的迷你ReactReact
- React虛擬dom和diff演算法React演算法
- React 虛擬 Dom 和 diff 演算法React演算法
- 從零開始實現React(一):JSX和虛擬DOMReactJS
- 談談虛擬dom和diff演算法演算法
- 解密虛擬 DOM——snabbdom 核心原始碼解讀解密原始碼
- [譯] 基於虛擬DOM(Snabbdom)的迷你ReactReact
- 詳解虛擬DOM與Diff演算法演算法
- 前端優化反應:虛擬dom解釋前端優化
- 將HTML字串編譯為虛擬DOM物件的基礎實現HTML字串編譯物件
- [譯] React效能優化-虛擬Dom原理淺析React優化
- 虛擬DOM和Diff演算法 - 入門級演算法
- 虛擬DOM和Diff演算法 – 入門級演算法
- React原始碼閱讀:虛擬DOM的初始化React原始碼
- 瞭解react、vue的一大核心技術:虛擬DOM的實現原理ReactVue
- 根據除錯工具看原始碼之虛擬dom(一)除錯原始碼
- 【React深入】深入分析虛擬DOM的渲染原理和特性React