vue源碼(五)-vue虛擬dom和diff對比
一、虛擬DOM
1、概念
虛擬DOM(Virtual DOM)是對DOM的JS抽象表示,它們是JS對象,能夠描述DOM結構和關係。應用程序 的各種狀態變化會作用於虛擬DOM,最終映射到DOM上。
2、優點
虛擬DOM輕量、快速:當它們發生變化時通過新舊虛擬DOM比對可以得到最小DOM操作量,從 而提升性能和用戶體驗。
跨平臺:將虛擬dom更新轉換爲不同運行時特殊操作實現跨平臺
兼容性:還可以加入兼容性代碼增強操作的兼容性
3、必要性
vue 1.0中有細粒度的數據變化偵測,它是不需要虛擬DOM的,但是細粒度造成了大量開銷,這對於大 型項目來說是不可接受的。因此,vue 2.0選擇了中等粒度的解決方案,每一個組件一個watcher實例, 這樣狀態變化時只能通知到組件,再通過引入虛擬DOM去進行比對和渲染。
4、源碼實現
通過文章vue源碼(四)-vue項目配置和入口文件,數據響應化處理分析發現,有虛擬dom掛載真實dom是由new Vue() => _init() => $mount()
這樣一個執行流程進行的。而$mount
最終的實現代碼來源於src/core/instance/lifecycle.js文件的mountComponent
查看該方法,發現196-202行處理了updateComponent
方法,updateComponent
的定義是189行。
在updateComponent
方法中發現調用順序是_render
然後是_update
。 其中_render
就是生成虛擬dom,然後由_update
更新到真實dom中。繼續查找_render
方法,打開文件src/core/instance/render.js
,查看_render
定義,可以看到83行定義了vnode,在89行通過傳遞參數vm.$createElement
調用render方法生成虛擬dom。
再來查看文件src/core/vdom/create-element.js
,99行開始進行虛擬dom的創建,首先對傳入的標籤tag進行判斷,由於創建時會存在原生標籤和自定義標籤兩種情況,所以通過判斷是否爲字符串進行處理。具體如下所示實現:
當tag爲字符串的時,判斷是否爲原生標籤,通過new Node()
方式進行創建虛擬dom,如果不是原生標籤,則進行判斷是否爲自定義組件,按照自定義組件進行處理,當以上條件不符合時,直接通過createComponent
進行創建虛擬dom。查看src/core/vdom/create-component.js
文件中createComponent方法,最終仍然是返回通過new VNode()
創建的虛擬dom。
下面我們使用真實代碼生成虛擬dom打印查看下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Vue.js elastic header example</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
{{message}}
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'hello world!初始化流程和入口文件!'
}
})
</script>
</body>
</html>
最終生成虛擬dom如下:
二、diff對比
生成的虛擬dom,最終是需要更新到真實dom中的,從之前代碼查看過程中,我們能夠知道最終的diff位於文件core\vdom\patch.js
,通過調用方法createPatchFunction進行渲染。
- patch中通過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間複雜度只 有O(n),是一種相當高效的算法。
- 同層級只做三件事:增刪改。具體規則是:new VNode不存在就刪;old VNode不存在就增;都存在就 比較類型,類型不同直接替換、類型相同執行更新;
該文件中方法patchVnode
進行執行更新操作:
兩個VNode類型相同,就執行更新操作,包括三種類型操作:屬性更新PROPS、文本更新TEXT、子節點更新REORDER
patchVnode具體規則如下:
- 如果新舊VNode都是靜態的,那麼只需要替換elm以及componentInstance即可。
- 新老節點均有children子節點,則對子節點進行diff操作,調用updateChildren
- 如果老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文本內容,然後爲當前DOM節
點加入子節點。 - 當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的所有子節點。
- 當新老節點都無子節點的時候,只是文本的替換。
文件中updateChildren
主要作用是用一種較高效的方式比對新舊兩個VNode的children得出最小操作補丁。執行一個雙循環是傳統方式,vue中針對web場景特點做了特別的算法優化
在新老兩組VNode節點的左右頭尾兩側都有一個變量標記,在遍歷過程中這幾個變量都會向中間靠攏。 當oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx時結束循環。
下面是遍歷規則:
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩交叉比較,共有4種比較 方法。
- 當 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 滿足sameVnode,直接將該 VNode節點進行patchVnode即可,不需再遍歷就完成了一次循環。
- 如果oldStartVnode與newEndVnode滿足sameVnode。說明oldStartVnode已經跑到了oldEndVnode 後面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的後面。
- 如果oldStartVnode與newEndVnode滿足sameVnode。說明oldStartVnode已經跑到了oldEndVnode 後面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的後面。
- 如果oldEndVnode與newStartVnode滿足sameVnode,說明oldEndVnode跑到了oldStartVnode的前 面,進行patchVnode的同時要將oldEndVnode對應DOM移動到oldStartVnode對應DOM的前面。
如果以上情況均不符合,則在old VNode中找與newStartVnode滿足sameVnode的vnodeToMove,若 存在執行patchVnode,同時將vnodeToMove對應DOM移動到oldStartVnode對應的DOM的前面。當然也有可能newStartVnode在old VNode節點中找不到一致的key,或者是即便key相同卻不是 sameVnode,這個時候會調用createElm創建一個新的DOM節點。459行是執行createElm過程。
至此循環結束,但是我們還需要處理剩下的節點。
當結束時oldStartIdx > oldEndIdx,這個時候舊的VNode節點已經遍歷完了,但是新的節點還沒有。說明了新的VNode節點實際上比老的VNode節點多,需要將剩下的VNode對應的DOM插入到真實DOM 中,此時調用addVnodes(批量調用createElm接口)。
但是,當結束時newStartIdx > newEndIdx時,說明新的VNode節點已經遍歷完了,但是老的節點還有 剩餘,需要從文檔中刪 的節點刪除。