聊聊vue2.5的patch過程(diff演算法)

jefferyE發表於2019-04-09

簡介

Vue2.0開始,引入了Virtual Dom,瞭解diff過程可以讓我們更高效的使用框架,必要時可以進行手工優化,本文針對的是Vue2.5.7版本中的Virtual Dom進行分析,力求以圖文並茂的方式來分析diff的過程。

其中patch過程中所用到的diff演算法來源於snabbdom

PS: 如有不對之處,還望指正。

什麼是VNode?

我們知道,瀏覽器中真實的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
}
複製程式碼

mounted過程都發生了什麼?

在瞭解patch過程之前,先來大概瞭解下mounted過程,我們知道,Vue最終會呼叫$mounted方法來進行掛載。 一般來說,Vue有兩條渲染路徑,分別對應生命週期中mounted和updated兩個鉤子函式,分別如下:

 1)元件例項初始化建立生成DOM

在該過程時,初始的Vnode為一個真實的DOM節點或者undefined(建立元件)

$mounted => mountComponent => updateComponent => _render => _update => patch => createElm => nodeOps.insert => removeVnodes

 1)元件資料更新時更新DOM

在該過程,初始化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流程圖:

\$mounted過程

patch原理分析

patch過程也是一個深度遍歷過程,比較只會在同層級進行,不會跨層級比較,借用一篇相當經典的文章 React’s diff algorithm中的圖,圖能很好的解釋該過程,如下:

React’s diff algorith
patch接收6個引數,其中兩個主要引數是vnode和oldVnode,也就是新舊兩個虛擬節點,下面詳細介紹下patch過程

1、patch邏輯

1、如果vnode不存在,而oldVnode存在,則呼叫invodeDestoryHook進行銷燬舊的節點
2、如果oldVnode不存在,而vnode存在,則呼叫createElm建立新的節點
3、如果oldVnode和vnode都存在
 1)如果oldVnode不是真實節點且和vnode是相同節點(呼叫sameVnode比較),則呼叫patchVnode進行patch
 2)如果oldVnode是真實DOM節點,則先把真實DOM節點轉為Vnode,再呼叫createElm建立新的DOM節點,並插入到真實的父節點中,同時呼叫removeVnodes將舊的節點從父節點中移除。

2、patchVnode邏輯

1、如果vnode和oldVnode完全一致,則什麼都不做處理,直接返回
2、如果oldVnode和vnode都是靜態節點,且具有相同的key,並且當vnode是克隆節點或是v-once指令控制的節點時,只需要把oldVnode的elm和oldVnode.children都複製到vnode上即可
3、如果vnode不是文字節點或註釋節點
 1)如果vnode的children和oldVnode的children都存在,且不完全相等,則呼叫updateChildren更新子節點
 2)如果只有vnode存在子節點,則呼叫addVnodes新增這些子節點
 3)如果只有oldVnode存在子節點,則呼叫removeVnodes移除這些子節點
 4)如果oldVnode和vnode都不存在子節點,但是oldVnode為文字節點或註釋節點,則把oldVnode.elm的文字內容置為空

4、如果vnode是文字節點或註釋節點,並且vnode.text和oldVnode.text不相等,則更新oldVnode的文字內容為vnode.text

3、updateChildren邏輯

updateChildren方法主要通過while迴圈去對比2棵樹的子節點來更新dom,通過對比新的來改變舊的,以達到新舊統一的目的。

1、如果oldStartVnode不存在,則將oldStartVnode設定為下一個節點
2、如果oldEndVnode不存在,則將oldEndVnode設定為上一個節點
3、如果oldStartVnode和newStartVnode是同一個節點(sameVnode),則呼叫patchVnode進行patch重複流程,同時將oldStartVnode和newStartVnode設定為下一個節點
4、如果oldEndVnode和newEndVnode是同一個節點(sameVnode),則呼叫patchVnode進行patch重複流程,同時將oldEndVnode和newEndVnode設定為上一個節點
5、如果oldStartVnode和newEndVnode是同一個節點(sameVnode),則呼叫patchVnode進行patch重複流程,同時將oldStartVnode設定為下一個節點,newEndVnode設定為上一個節點,需要對DOM進行移動
6、如果oldEndVnode和newStartVnode是同一個節點(sameVnode),則呼叫patchVnode進行patch重複流程,同時將oldEndVnode設定為上一個節點,newStartVnode設定為下一個節點,需要對DOM進行移動
7、否則,嘗試在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之間的節點

附上一張流程圖:

patch過程
針對以上過程,對其中的各個情況都分別簡單舉個例子,進行分析,可以自行debugger

情況一:oldStartVnode和newStartVnode是相同節點

情況一
情況二:oldEndVnode和newEndVnode是相同節點

情況二
情況三:oldStartVnode和newEndVnode是相同節點

情況三
情況四:oldEndVnode和newStartVnode是相同節點

情況四
情況五:oldStartVnode、oldEndVnode、newStartVnode和newEndVnode都不是相同節點

情況五
附上總圖:

總圖

小結

1、不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key後,除了頭尾兩端的比較外,還會從用key生成的物件oldKeyToIdx中查詢匹配的節點,所以為節點設定key可以更高效的利用dom。

2、diff的遍歷過程中,只要是對dom進行的操作都呼叫nodeOps.insertBefore,nodeOps.insertBefore只是原生insertBefore的簡單封裝。
比較分為兩種,一種是有vnode.key的,一種是沒有的。但這兩種比較對真實dom的操作是一致的。

3、對於與sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)為true的情況,不需要對dom進行移動。

相關文章