Vue2.0開始,引入了Virtual Dom,瞭解diff過程可讓咱們更高效的使用框架,必要時能夠進行手工優化,本文針對的是Vue2.5.7版本中的Virtual Dom進行分析,力求以圖文並茂的方式來分析diff的過程。node
其中patch過程當中所用到的diff算法來源於snabbdomgit
PS: 若有不對之處,還望指正。github
咱們知道,瀏覽器中真實的DOM節點對象上的屬性和方法比較多,若是每次都生成新的DOM對象,對性能是一種浪費,在這種狀況下,Virtual Dom出現了,而VNode是用來模擬真實DOM節點,即把真實DOM樹抽象成用JavaScript對象構成的抽象樹,從而能夠對這顆抽象樹進行建立節點、刪除節點以及修改節點等操做,在這過程當中都不須要操做真實DOM,只須要操做JavaScript對象,當數據發生改變時,在改變真實DOM節點以前,會先比較相應的VNode的的數據,若是須要改變,才更新真實DOM,大大提高了性能。同時VNode不依賴平臺。算法
具體能夠經過如下代碼查看標準DOM對象上的方法和屬性瀏覽器
const dom = document.createElement('div');
for (let key in dom) {
console.log(key)
}
複製代碼
VNode構造函數具體結構以下(具體見源碼):框架
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
fnScopeId: ?string; // functional scope id support
}
複製代碼
在瞭解patch過程以前,先來大概瞭解下mounted過程,咱們知道,Vue最終會調用$mounted方法來進行掛載。 通常來講,Vue有兩條渲染路徑,分別對應生命週期中mounted和updated兩個鉤子函數,分別以下:dom
在該過程時,初始的Vnode爲一個真實的DOM節點或者undefined(建立組件)async
$mounted => mountComponent => updateComponent => _render => _update => patch => createElm => nodeOps.insert => removeVnodes函數
在該過程,初始化Vnode爲以前的prevVnode,不是真實DOM節點性能
flushSchedulerQueue => watcher.run => watcher.get => updateComponent => _render => _update => patch => patchVnode => updateChildren
其中,_render函數內部則是調用createElement方法將渲染函數轉爲VNode,而_update函數則是在內部調用patch方法將VNode轉化爲真實的DOM節點。
createElement和patch過程是一個深度遍歷過程,也就是"先子後父",即先調用子類的mounted或updated鉤子方法,在調用父類的該鉤子。
附上一張$mounted流程圖:
patch過程也是一個深度遍歷過程,比較只會在同層級進行,不會跨層級比較,借用一篇至關經典的文章 React’s diff algorithm中的圖,圖能很好的解釋該過程,以下:
一、若是vnode不存在,而oldVnode存在,則調用invodeDestoryHook進行銷燬舊的節點
二、若是oldVnode不存在,而vnode存在,則調用createElm建立新的節點
三、若是oldVnode和vnode都存在
1)若是oldVnode不是真實節點且和vnode是相同節點(調用sameVnode比較),則調用patchVnode進行patch
2)若是oldVnode是真實DOM節點,則先把真實DOM節點轉爲Vnode,再調用createElm建立新的DOM節點,並插入到真實的父節點中,同時調用removeVnodes將舊的節點從父節點中移除。
一、若是vnode和oldVnode徹底一致,則什麼都不作處理,直接返回
二、若是oldVnode和vnode都是靜態節點,且具備相同的key,而且當vnode是克隆節點或是v-once指令控制的節點時,只須要把oldVnode的elm和oldVnode.children都複製到vnode上便可
三、若是vnode不是文本節點或註釋節點
1)若是vnode的children和oldVnode的children都存在,且不徹底相等,則調用updateChildren更新子節點
2)若是隻有vnode存在子節點,則調用addVnodes添加這些子節點
3)若是隻有oldVnode存在子節點,則調用removeVnodes移除這些子節點
4)若是oldVnode和vnode都不存在子節點,可是oldVnode爲文本節點或註釋節點,則把oldVnode.elm的文本內容置爲空
四、若是vnode是文本節點或註釋節點,而且vnode.text和oldVnode.text不相等,則更新oldVnode的文本內容爲vnode.text
updateChildren方法主要經過while循環去對比2棵樹的子節點來更新dom,經過對比新的來改變舊的,以達到新舊統一的目的。
一、若是oldStartVnode不存在,則將oldStartVnode設置爲下一個節點
二、若是oldEndVnode不存在,則將oldEndVnode設置爲上一個節點
三、若是oldStartVnode和newStartVnode是同一個節點(sameVnode),則調用patchVnode進行patch重複流程,同時將oldStartVnode和newStartVnode設置爲下一個節點
四、若是oldEndVnode和newEndVnode是同一個節點(sameVnode),則調用patchVnode進行patch重複流程,同時將oldEndVnode和newEndVnode設置爲上一個節點
五、若是oldStartVnode和newEndVnode是同一個節點(sameVnode),則調用patchVnode進行patch重複流程,同時將oldStartVnode設置爲下一個節點,newEndVnode設置爲上一個節點,須要對DOM進行移動
六、若是oldEndVnode和newStartVnode是同一個節點(sameVnode),則調用patchVnode進行patch重複流程,同時將oldEndVnode設置爲上一個節點,newStartVnode設置爲下一個節點,須要對DOM進行移動
七、不然,嘗試在oldChildren中查找與newStartVnode具備相同key的節點
1)若是沒有找到,則說明newStartVnode是一個新節點,則調用createElem建立一個新節點,同時將newStartVnode設置爲下一個節點
2)若是找到了具備相同key的節點
(1)若是找到的節點與newStartVnode是同一個節點(sameVnode),則調用patchVnode進行patch重複流程,同時把newStartVnode.elm移動到oldStartVnode.elm以前,並把newStartVnode設置爲下一個節點,須要對DOM進行移動
(2)不然,調用createElm建立一個新的節點,同時把newStartVnode設置爲下一個節點
上述過程當中,若是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,即oldChildren和newChildren節點在遍歷過程當中若是任意一個的開始索引和結束索引重合,則代表遍歷結束。
遍歷結束後,還需針對oldChildren和newChildren沒有遍歷的節點進行處理,分爲如下兩種狀況:
1)若是oldStartIdx大於oldEndIdx,說明newChildren可能還未遍歷完,則須要調用addVnodes添加newStartIdx到newEndIdx之間的節點
2)若是newStartIdx大於newEndIdx,說明oldChildren可能還未遍歷完,則須要調用removeVnodes移除oldStartIdx到oldEndIdx之間的節點
附上一張流程圖:
狀況一:oldStartVnode和newStartVnode是相同節點
一、不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key後,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,因此爲節點設置key能夠更高效的利用dom。
二、diff的遍歷過程當中,只要是對dom進行的操做都調用nodeOps.insertBefore,nodeOps.insertBefore只是原生insertBefore的簡單封裝。
比較分爲兩種,一種是有vnode.key的,一種是沒有的。但這兩種比較對真實dom的操做是一致的。
三、對於與sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)爲true的狀況,不須要對dom進行移動。