使用過Vue和React的小夥伴確定對虛擬Dom和diff算法很熟悉,它扮演着很重要的角色。因爲小編接觸Vue比較多,React只是淺學,因此本篇主要針對Vue來展開介紹,帶你一步一步搞懂它。javascript
虛擬DOM(Virtual Dom),也就是咱們常說的虛擬節點,是用JS對象來模擬真實DOM中的節點,該對象包含了真實DOM的結構及其屬性,用於對比虛擬DOM和真實DOM的差別,從而進行局部渲染來達到優化性能的目的。html
真實的元素節點:vue
<div id="wrap">
<p class="title">Hello world!</p>
</div>
複製代碼
VNode:java
{
tag:'div',
attrs:{
id:'wrap'
},
children:[
{
tag:'p',
text:'Hello world!',
attrs:{
class:'title',
}
}
]
}
複製代碼
簡單瞭解虛擬DOM後,是否是有小夥伴會問:Vue和React框架中爲何會用到它呢?好問題!那來解決下小夥伴的疑問。node
起初咱們在使用JS/JQuery時,不可避免的會大量操做DOM,而DOM的變化又會引起迴流或重繪,從而下降頁面渲染性能。那麼怎樣來減小對DOM的操做呢?此時虛擬DOM應用而生,因此虛擬DOM出現的主要目的就是爲了減小頻繁操做DOM而引發迴流重繪所引起的性能問題的!算法
說到這裏,那麼虛擬DOM和真實DOM的區別是什麼呢?總結大概以下:api
總損耗 = 真實DOM增刪改 + (多節點)迴流/重繪; //計算使用真實DOM的損耗
總損耗 = 虛擬DOM增刪改 + (diff對比)真實DOM差別化增刪改 + (較少節點)迴流/重繪; //計算使用虛擬DOM的損耗
複製代碼
能夠發現,都是圍繞頻繁操做真實DOM引發迴流重繪,致使頁面性能損耗來講的。不過框架也不必定非要使用虛擬DOM,關鍵在於看是否頻繁操做會引發大面積的DOM操做。數組
那麼虛擬DOM究竟經過什麼方式來減小了頁面中頻繁操做DOM呢?這就不得不去了解DOM Diff算法了。瀏覽器
當數據變化時,vue如何來更新視圖的?其實很簡單,一開始會根據真實DOM生成虛擬DOM,當虛擬DOM某個節點的數據改變後會生成一個新的Vnode,而後VNode和oldVnode對比,把不一樣的地方修改在真實DOM上,最後再使得oldVnode的值爲Vnode。markdown
diff過程就是調用patch函數,比較新老節點,一邊比較一邊給真實DOM打補丁(patch);
對照vue源碼來解析一下,貼出核心代碼,旨在簡單明瞭講述清楚,否則小編本身看着都頭大了O(∩_∩)O
那麼patch是怎樣打補丁的?
//patch函數 oldVnode:老節點 vnode:新節點
function patch (oldVnode, vnode) {
...
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode) //若是新老節點是同一節點,那麼進一步經過patchVnode來比較子節點
} else {
/* -----不然新節點直接替換老節點----- */
const oEl = oldVnode.el // 當前oldVnode對應的真實元素節點
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根據Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 將新元素添加進父元素
api.removeChild(parentEle, oldVnode.el) // 移除之前的舊元素節點
oldVnode = null
}
}
...
return vnode
}
//判斷兩節點是否爲同一節點
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 標籤名
a.isComment === b.isComment && // 是否爲註釋節點
// 是否都定義了data,data包含一些具體信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 當標籤是<input>的時候,type必須相同
)
}
複製代碼
從上面能夠看出,patch函數是經過判斷新老節點是否爲同一節點:
若是是同一節點,執行patchVnode進行子節點比較;
若是不是同一節點,新節點直接替換老節點;
那若是不是同一節點,可是它們子節點同樣怎麼辦嘞?OMG,要牢記:diff是同層比較,不存在跨級比較的!簡單提一嘴,React中也是如此,它們只是針對同一層的節點進行比較。
既然到了patchVnode方法,說明新老節點爲同一節點,那麼這個方法作了什麼處理?
function patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el //找到對應的真實DOM
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return //若是新老節點相同,直接返回
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
//若是新老節點都有文本節點且不相等,那麼新節點的文本節點替換老節點的文本節點
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
//若是新老節點都有子節點,執行updateChildren比較子節點[很重要也很複雜,下面展開介紹]
updateChildren(el, oldCh, ch)
}else if (ch){
//若是新節點有子節點而老節點沒有子節點,那麼將新節點的子節點添加到老節點上
createEle(vnode)
}else if (oldCh){
//若是新節點沒有子節點而老節點有子節點,那麼刪除老節點的子節點
api.removeChildren(el)
}
}
}
複製代碼
若是兩個節點不同,直接用新節點替換老節點;
若是兩個節點同樣,
最複雜的狀況也就是新老節點都有子節點,那麼updateChildren是如何來處理這一問題的,該方法也是diff算法的核心,下面咱們來了解一下!
因爲代碼太多了,這裏先作個概述。updateChildren方法的核心:
下面結合圖來理解:
第一步:
oldStartIdx = A , oldEndIdx = C;
newStartIdx = A , newEndIdx = D;
複製代碼
此時oldStartIdx和newStarIdx匹配,因此將dom中的A節點放到第一個位置,此時A已經在第一個位置,因此不作處理,此時真實DOM順序:A B C;
第二步:
oldStartIdx = B , oldEndIdx = C;
newStartIdx = C , oldEndIdx = D;
複製代碼
此時oldEndIdx和newStartIdx匹配,將本來的C節點移動到A後面,此時真實DOM順序:A C B;
第三步:
oldStartIdx = C , oldEndIdx = C;
newStartIdx = B , newEndIdx = D;
oldStartIdx++,oldEndIdx--;
oldStartIdx > oldEndIdx
複製代碼
此時遍歷結束,oldCh已經遍歷完,那麼將剩餘的ch節點根據本身的index插入到真實DOM中便可,此時真實DOM順序:A C B D;
因此匹配過程當中判斷結束有兩個條件:
看下圖這個實例,就是新節點先遍歷完成刪除多餘節點:
最後,在這些子節點sameVnode後若是知足條件繼續執行patchVnode,層層遞歸,直到oldVnode和Vnode中全部子節點都比對完成,也就把全部的補丁都打好了,此時更新到視圖。
dom的diff算法時間複雜度爲o(n^3),若是使用在框架中性能會不好。Vue使用的diff算法,時間複雜度爲o(n),簡化了不少操做。
最後,用一張圖來記憶整個Diff過程,但願你能有所收穫!
由於React只是簡單學了基礎,這裏做爲對比來概述一下:
1.React渲染機制:React採用虛擬DOM,在每次屬性和狀態發生變化時,render函數會返回不一樣的元素樹,而後對比返回的元素樹和上次渲染樹的差別並對差別部分進行更新,最後渲染爲真實DOM。
2.diff永遠都是同層比較,若是節點類型不一樣,直接用新的替換舊的。若是節點類型相同,就比較他們的子節點,依次類推。一般元素上綁定的key值就是用來比較節點的,因此必定要保證其惟一性,通常不採用數組下標來做爲key值,由於當數組元素髮生變化時index會有所改動。
3.渲染機制的整個過程包含了更新操做,將虛擬DOM轉換爲真實DOM,因此整個渲染過程就是Reconciliation。而這個過程的核心又主要是diff算法,利用的是生命週期shouldComponentUpdate函數。