# Virtual Dom
## "昂貴"的DOM
我們可以做個試驗。打印出一個空元素的第一層屬性,可以看到標準讓元素實現的東西太多了。如果每次都重新生成新的元素,對性能是巨大的浪費。

#### Virtual Dom就是解決這個問題的一個思路,到底什么是Virtual Dom呢?
>通俗易懂的來說就是用一個簡單的JS對象去代替復雜的dom對象。
舉個簡單的例子,我們在body里插入一個class為a的div。
```js
var mydiv = document.createElement('div');
mydiv.className = 'a';
document.body.appendChild(mydiv);
```
對于這個div我們可以用一個簡單的對象VNode代表它,它存儲了對應dom的一些重要參數,在改變dom之前,會先比較相應虛擬dom的數據,如果需要改變,才會將改變應用到真實dom上。
```js
//偽代碼
var VNode = {
tagName: 'DIV',
className: 'a'
};
mydiv.className = 'b'; // 改變class屬性
var oldVNode = VNode
var newVNode = {
tagName: 'DIV',
className: 'b'
}
if(oldVNode.tagName !== newVNode.tagName || oldVNode.className !== newVNode.className){
patch(mydiv)
}
```
#### 讀到這里就會產生一個疑問,為什么不直接修改dom而需要加一層Vrtual Dom呢?
> 很多時候手工優化dom確實會比virtual dom效率高,對于比較簡單的dom結構用手工優化沒有問題,但當頁面結構很龐大,結構很復雜時,手工優化會花去大量時間,而且可維護性也不高,不能保證每個人都有手工優化的能力。
> 至此,Vrtual Dom的解決方案應運而生,Vrtual Dom很多時候都不是最優的操作,但它具有普適性,在效率、可維護性之間達平衡。
## Vrtual Dom更新過程的實現
> 網上有太多的人講Vrtual Dom的實現過程。特別是其中的diff算法。但是,對于新手來說,這些文章會讓你似懂非懂。
> 我覺得最主要的原因是沒有對`key`這個屬性進行很好的解釋。
首先介紹兩個相關概念
### diff 算法
VDom因為是純粹的JS對象,所以操作它會很高效,但是VDom的變更最終會轉換成DOM操作,為了實現高效的DOM操作,一套高效的虛擬DOM diff算法顯得很有必要。
先看整體視圖,整個diff分兩部分:

#### (一)、優先處理特殊場景
(1)、頭部的同類型節點、尾部的同類型節點
>這類節點更新前后位置沒有發生變化,所以不用移動它們對應的DOM
(2)、頭尾/尾頭的同類型節點
>這類節點位置很明確,不需要再花心思查找,直接移動DOM就好
處理了這些場景之后,一方面一些不需要做移動的DOM得到快速處理,另一方面待處理節點變少,縮小了后續操作的處理范圍,性能也得到提升
#### (二)、“原地復用”
原地復用”是指Vue會盡可能復用DOM,盡可能不發生DOM的移動。
> Vue在判斷更新前后指針是否指向同一個節點,其實不要求它們真實引用同一個DOM節點,實際上它僅判斷指向的是否是同類節點(比如2個不同的div,在DOM上它們是不一樣的,但是它們屬于同類節點),如果是同類節點,那么Vue會直接復用DOM,這樣的好處是不需要移動DOM。
#### (三)、
### key屬性在列表渲染中的作用
**官方解釋:**
> 當 Vue.js 用 v-for 正在更新已渲染過的元素列表時,它默認用“就地復用”策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序, 而是簡單復用此處每個元素,并且確保它在特定索引下顯示已被渲染過的每個元素。這個類似 Vue 1.x 的 track-by="$index" 。
> 為了給 Vue 一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key 屬性。理想的 key 值是每項都有的且唯一的 id。這個特殊的屬性相當于 Vue 1.x 的 track-by ,但它的工作方式類似于一個屬性,所以你需要用 v-bind 來綁定動態值
我對官方的提煉:
* 節點識別
* “原地復用”策略
* 默認類似 track-by="$index"
#### 下面將從列表元素的渲染更新過程來介紹Vrtual Dom更新過程

Vue分為在oldVDom樹設置oldStart和oldEnd指針,為newVDom樹設置newStart和newEnd指針。如下圖所示:

新、舊虛擬DOM樹比較的的過程就是調用patch函數,就像打補丁一樣修改真實dom。同時,修改相應指針的指向,使其循環遍歷所有同層節點。
```
// 偽代碼
function patch() {
if (oldVNode.key === newVNode.key) {
// 如果兩VNode節點的身份標識符相同(key)
// 則對oldVNode進行更新操作
updateChildren(oldVNode, newVNode);
} else if ( vnode.el === oldVnode.el) {
// 如果兩VNode節點屬于同類型
// 則對oldVNode進行復用操作
patchVnode(oldVNode, newVNode)
} else if (!oldVNode.el && newVNode.el) {
// 如果oldVNode中不存在,newVNode中存在
// 則對oldVNode執行插入操作
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
let newDom = createEle(vnode)
api.insertBefore(parentEle, newDom, api.nextSibling(oEl))
} else if (oldVNode.el && !newVNode.el) {
// 如果oldVNode中存在,newVNode中不存在
// 則對oldVNode執行刪除操作
let parentEle = api.parentNode(oEl)
api.removeChild(parentEle, oldVnode.el)
}
}
```
具體過程如下圖所示

最后patch過程為

可以看到:
> 對A、B節點進行更新;
> 對C、D節點進行復用;
> 插入D節點。
顯然,更新的性能損耗要小于復用。因此,這樣的更新效率是比較低的。但是,這就是diff 算法的“原地復用”策略。
現在我們來看看當存在key屬性是的patch過程
設置key和不設置key的區別:
> 不設key,oldVNode和newVNode只會進行頭尾兩端的相互比較,設key后,除了頭尾兩端的比較外,還會從用key生成的對象`oldKeyToIdx`中查找匹配的節點。
```js
oldKeyToIdx = createKeyToOldIdx(oldVNode, oldStart, oldEnd) // 有key生成index表
idxInOld = oldKeyToIdx[newStart.key]
```
> 所以為節點設置key可以更高效的利用dom。

可以看到:
> 對A、B、C、D節點進行更新;
> 在正確的位置插入F節點;
通過虛擬DOM計算出兩顆虛擬DOM樹之間的差異后,我們就可以用盡可能小的代價修改真實的DOM樹結構。在效率、可維護性之間達平衡。
至此,關于Vue的虛擬DOM的內容講完了,想更加深入了解的可以直接看源碼。我相信你看懂我這篇文章再看源碼應該難度會小很多。