diff算法
vdom
因為是純粹的JS
對象,所以操作它會很高效,但是vdom
的變更最終會轉換成DOM
操作,為了實現高效的DOM
操作,一套高效的虛擬DOM diff
算法顯得很有必要。
Vue
的diff
算法僅在同級的vnode
間做diff
,遞歸地進行同級vnode
的diff
,最終實現整個DOM
樹的更新。
oldStart
+oldEnd
,newStart
+newEnd
這樣2對指針,分別對應oldVdom
和newVdom
的起點和終點。起止點之前的節點是待處理的節點,Vue
不斷對vnode
進行處理同時移動指針直到其中任意一對起點和終點相遇。處理過的節點Vue
會在oldVdom
和newVdom
中同時將它標記為已處理。
Vue
通過diff
算法來比較新老vdom
的差異,計算出需要操作dom
的最少次數,實際中并以下措施來提升diff
的性能。
- 1、優先處理特殊場景:處理頭尾同類型節點。
- 2、原地復用:盡可能復用
DOM
,盡可能不發生DOM
的移動。Vue
在判斷更新前后指針是否指向同一個節點,其實不要求它們真實引用同一個DOM
節點,實際上它僅判斷指向的是否是同類節點(比如2個不同的div
,在DOM
上它們是不一樣的,但是它們屬于同類節點),如果是同類節點,那么Vue
會直接復用DOM
,這樣的好處是不需要移動DOM
。
整體視圖
處理頭尾同類型節點,即
oldStart
和newStart
指向同類節點的情況 /oldEnd
和newEnd
指向同類節點的情況,直接移動指針。處理頭尾、尾頭同類型節點,即
oldStart
和newEnd
,以及oldEnd
和newStart
指向同類節點的情況,互換位置。處理新增的節點:在
oldVdom
中找不到節點,說明它是新增的,那么就創建一個新的節點,插入DOM
樹,插到什么位置?插到oldStart
指向的節點前面,然后將newStart
后移1位標記為已處理。oldStart
不需要移動,因為oldVdom
中沒有這個節點。處理更新的節點:
newStart
來到的位置,在oldVdom
中能找到它而且不在指針位置(查找oldVdom
中oldStart
到oldEnd
區間內的節點),說明它的位置移動了,那么需要在DOM
樹中移動它,移到哪里?移到oldStart
指向的節點前面,與此同時將節點標記為已處理(因為最后oldVdom
中此時還是存在這個節點的,之后指針會游到該節點的位置,如果被標記過已經處理了,則是需要出現在新DOM
中的節點,不要刪除它,而是之前就以前移動過它了),跟前面幾種情況有點不同,newVdom
中該節點在指針下,可以移動newStart
進行標記,而在oldVdom
中該節點不在指針處,所以采用設置為undefined
的方式來標記處理刪除的節點:如果
newStart
跨過了newEnd
,它們相遇啦!而這個時候,oldStart
和oldEnd
還沒有相遇,說明這2個指針之間的節點是此次更新中被刪掉的節點。而如果有之前被標記過的節點,則不做處理,只刪除此時沒有標記過的節點。
在應用中也可能會遇到oldVdom
的起止點相遇了,但是newVdom
的起止點沒有相遇的情況,這個時候需要對newVdom
中的未處理節點進行處理,這類節點屬于更新中被加入的節點,需要將他們插入到DOM
樹中。
第一部分是一個循環,循環內部是一個分支邏輯,每次循環只會進入其中的一個分支,每次循環會處理一個節點,處理之后將節點標記為已處理(oldVdom
和newVdom
都要進行標記,如果節點只出現在其中某一個vdom
中,則另一個vdom
中不需要進行標記),標記的方法有2種,當節點正好在vdom
的指針處,移動指針將它排除到未處理列表之外即可,否則就要采用其他方法,Vue
的做法是將節點設置為undefined
。
循環結束之后,可能newVdom
或者oldVdom
中還有未處理的節點,如果是newVdom
中有未處理節點,則這些節點是新增節點,做新增處理。如果是oldVdom
中有這類節點,則這些是需要刪除的節點,相應在DOM
樹中刪除之
整個過程是逐步找到更新前后vdom
的差異,然后將差異反應到DOM
樹上(也就是patch
),特別要提一下Vue
的patch
是即時的,并不是打包所有修改最后一起操作DOM
(React
則是將更新放入隊列后集中處理)
虛擬dom
1、dom
很慢
當創建一個元素比如div
,有以下幾項內容需要實現: HTML element
、Element
、GlobalEventHandler
。簡單的說,就是插入一個dom
元素的時候,這個元素上本身或者繼承很多屬性如 width
、height
、offsetHeight
、style
、title
,另外還需要注冊這個元素的諸多方法,比如onfucos
、onclick
等等。 這還只是一個元素,如果元素比較多的時候,還涉及到嵌套,那么元素的屬性和方法等等就會很多,效率很低。
尤其是在js
操作DOM
的過程中,不僅有dom
本身的繁重,js
的操作也需要浪費時間,我們認為js
和DOM
之間有一座橋,如果你頻繁的在橋兩邊走動,顯然效率是很低的。
虛擬dom
就是解決這個問題的! 雖然它解決不了dom
自身的繁重(虛擬dom
只實現了真實dom
的重要的屬性和事件,但是最終渲染在頁面的真實dom
依然是繁重的),但是虛擬dom
可以對js
操作dom
這一部分內容進行優化。
2、設計
隔離dom
并不僅僅是因為dom
慢,而也是為了把界面和業務完全隔離,操作數據的只關心數據,操作界面的只關心界面。
即我提供一個Component
,然后你只管給我數據,界面的事情完全不用你操心,我保證會把界面變成你想要的樣子。所以說著力點就在于View
層。只要你給的數據是[1, 2, 3]
,我保證顯示的是[1, 2, 3]
。沒有什么刪除一個Element
,添加一個Element
這樣的事情。NO。你要我顯示什么就給我一個完整的列表。
3、實現
首先,vdom
并沒有完全實現dom
,即vdom
和真正地dom
是不一樣的,vdom
最主要的還是保留了Element
之間的層次關系和一些基本屬性。因為真實dom
實在是太復雜,一個空的Element
都復雜得能讓你崩潰,并且幾乎所有內容我根本不關心好嗎。所以vdom
里每一個Element
實際上只有幾個屬性,即最重要的,最為有用的,并且沒有那么多亂七八糟的引用,比如一些注冊的屬性和函數啊,這些都是默認的,創建vdom
進行diff
的過程中大家都一致,是不需要進行比對的。所以哪怕是直接把vdom
刪了,根據新傳進來的數據重新創建一個新的vdom
出來都非常非常非常快。。
所以,引入了vdom
之后,你給我一個數據,我根據這個數據生成一個全新的vdom
,然后跟我上一次生成的vdom
去 diff,得到一個Patch
,然后把這個Patch
打到瀏覽器的dom
上去。完事。并且這里的patch
顯然不是完整的vdom
,而是新的vdom
和上一次的vdom
經過diff
后的差異化的部分。
假設在任意時候有,vdom1
== dom1
(組織結構相同, 顯然vdom
和真實dom
是不可能完全相等的)。當有新數據來的時候,我生成vdom2
,然后去和vdom1
做diff
,得到一個Patch
(差異化的結果)。然后將這個Patch
去應用到dom1
上,得到dom2
。如果一切正常,那么有vdom2
== dom2
(同樣是結構上的相等)。此時vdom2
就會與下一次vdom3
進行比較。
1.用JavaScript
對象來表示DOM
樹的結構; 然后用這個樹構建一個真正的DOM
樹,插入到文檔中。
2.當狀態變更的時候,重新構造一個新的對象樹,然后用這個新的樹和舊的樹作對比,記錄兩個樹的差異。
3.把2所記錄的差異應用在步驟一所構建的真正的DOM
樹上,視圖就更新了。
差異化實現
差異類型
1.替換原來的節點,如把上面的div換成了section。
2.移動、刪除、新增子節點, 例如上面div的子節點,把p和ul順序互換。
3.修改了節點的屬性。
4.對于文本節點,文本內容可能會改變。
所以,我們可以定義下面的幾種類型:
var REPLACE = 0;
var REORDER = 1;
var PROPS = 2;
var TEXT = 3;
對于節點替換,很簡單,判斷新舊節點的tagName是不是一樣的,如果不一樣的說明需要替換掉。 如div換成了section,就記錄下:
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]
除此之外,還新增了屬性id為container,就記錄下:
pathches[0] = [
{
type: REPLACE,
node: newNode
},
{
type: PROPS,
props: {
id: 'container'
}
}
]
如果是文本節點發生了變化,那么就記錄下:
pathches[2] = [
{
type: TEXT,
content: 'virtual DOM2'
}
]
列表對比算法
a b c d e f g h i => a b c h d f g i j
現在對節點進行了刪除、插入、移動的操作。新增j節點,刪除e節點,移動h節點,現在知道了新舊的順序,求最小的插入、刪除操作(移動可以看成是刪除和插入操作的結合),優化操作次數,我們能夠獲取到某個父節點的子節點的操作,就可以記錄下來:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
把差異引用到真正的DOM樹上
因為步驟一所構建的 JavaScript 對象樹(Vdom)和render出來真正的DOM樹的信息、結構是一樣的。所以我們可以對那棵DOM樹也進行深度優先的遍歷,遍歷的時候從步驟二生成的patches對象中找出當前遍歷的節點差異,然后進行 DOM 操作。
function patch (node, patches) {
var walker = {index: 0}
dfsWalk(node, walker, patches)
}
function dfsWalk (node, walker, patches) {
var currentPatches = patches[walker.index] // 從patches拿出當前節點的差異
var len = node.childNodes ? node.childNodes.length: 0
for (var i = 0; i < len; i++) { // 深度遍歷子節點
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
if (currentPatches) {
applyPatches(node, currentPatches) // 對當前節點進行DOM操作
}
}
function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}