總共寫了四篇文章(都是本身的一些拙見,僅供參考,請多多指教,我這邊也會持續修正加更新)html
這篇主要是說一下snabbdom的diff算法
node
在上一篇中我總結過:
對比渲染的流程大致分爲
1.經過sameVnode來判斷兩個vnode是否值得進行比較
2.若是不值得,直接刪除舊的vnode,渲染新的vnode
3.若是值得,調用模塊鉤子函數,對其節點的屬性進行替換,例如style,event等;再判斷節點子節點是否爲文本節點,若是爲文本節點則進行更替,若是還存在其餘子節點則調用updateChildren,對子節點進行更新,更新流程將會回到第一步,重複;算法
這篇文章的重點就是說一下updateChildren這個函數segmentfault
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
這是一個比較兩個vnode是否類似,是否值得去進行比較的函數,那麼這裏爲何會提到它?由於這裏面有一個很重要的值---key
在平時的使用中幾乎用不到這個key值,不會去專門給它一個定義值,由於undefined===undefined,不會影響其比較;數組
key值的出現主要是爲了應付一些場景
例如:dom
<ul> <ul> <li>1</li> <li>2</li> <li>2</li> --> <li>3</li> <li>3</li> <li>4</li> </ul> </ul>
對於這種狀況,若是按照正常的作法,就是一個個vnode去進行比較,發現其文本節點不對,就會一個個進行替換例如:<li>1</li>--><li>2</li> .... 這樣就會進行三次dom操做函數
對於這種狀況是否能夠優化呢?
答案是能夠的,咱們能夠刪除<li>1</li>,而後添加一個<li>4</li>,這樣就只進行了兩次dom操做就完成了須要的效果優化
那這裏就涉及到一個標記值,標記着在新vnode中還有哪些舊的vnode存在,key值就是充當着這個角色。指針
[1(key:a),2(key:b),3(key:c)]code
[2(key:b),3(key:c),4(key:d)]
[a,b,c] -> [a(x),b,c,d(+)] === [1,2,3] --> [1(x),2,3,4(+)]
key值與vnode造成了一個映射,能夠看到,咱們經過對key值的排序、增刪間接完成了對vnode的操做,使用最少的dom操做來完成了
那這裏就會有一個問題,咱們如何完成上面的操做呢?這個過程咱們能夠理解爲一種優化對比渲染的過程,也就是diff算法的核心
我這邊舉一個複雜的例子,記錄每一步的操做:
下面是頁面真實的dom,分別保存在本身vnode的elm屬性上;舊-->新
<ul> <ul> <li>a</li> <li>a</li> <li>b</li> <li>d</li> <li>c</li> <li>f</li> <li>f</li> <li>h</li> <li>e</li> --> <li>k</li> <li>d</li> <li>b</li> <li>g</li> <li>g</li> </ul> </ul>
假設每一個元素都有一個key值一一對應,且不重複,它們的key值分別爲
a:1 a:1 b:2 d:6 c:3 f:4 f:4 --> h:8 e:5 k:9 d:6 b:2 g:7 g:7
將舊新vnode分別放入兩個數組
old:[vnode,....] new:[vnode,....]
其實咱們是比較其key值是否相等,而後再決定如何排序,增刪vnode的位置,patch vnode,最終達到改變dom的目的,爲了方便理解,我這裏把其key值拿出來放入一個數組,每個key在數組中的索引都對應着相應的vnode在其數組中的索引,在真實代碼中是直接比較vnode.key值。
oldKey:[1,2,3,4,5,6,7] oldStartIdx:0 oldStartVal:1 oldEndIdx:6 oldEndVal:7 newKey:[1,6,4,8,9,2,7] newStartIdx:0 newStartVal:1 newEndIdx:6 newEndVal:7
用的是雙指針的方法,頭尾同時開始掃描;
循環兩個數組,循環條件爲(old_startIndex <= old_endIndex && new_startIndex <= new_endIndex)
(下面說的patch是直接對vnoe.elm進行修改,調用前面的patchVnode函數,也就是直接對頁面的dom進行修改,及時比較及時修改)
比較oldStartVal和newStartVal是否相等,若是相等則oldStartIdx和newStartIdx分別加1,並對oldStartVal對應的vnode進行patch,進入下一次循環;這個例子中oldStartVal==newStartVal,因此oldStartIdx:1 newStartIdx:1;若不相等,繼續比較;
比較事後: oldStartIdx:1 oldEndIdx:6 oldStartVal:2 oldEndVal:7 newStartIdx:1 newEndIdx:6 newStartVal:6 newEndVal:7 比較範圍縮小後: oldKey:[2,3,4,5,6,7] newKey:[6,4,8,9,2,7] dom: <li>a</li> <li>b</li> <li>c</li> <li>f</li> <li>e</li> <li>d</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,f,e,d,g]
比較oldEndVal和newEndVal是否相等,若是相等則oldEndIdx和newEndIdx分別減1,並對oldEndVal對應的舊vnode進行patch,進入下一次循環;這裏oldEndVal==newEndVal,因此oldEndIdx:5 newEndIdx:5;若不相等,繼續比較;
比較事後: oldStartIdx:1 oldEndIdx:5 oldStartVal:2 oldEndVal:6 newStartIdx:1 newEndIdx:5 newStartVal:6 newEndVal:2 比較範圍縮小後: oldKey:[2,3,4,5,6] newKey:[6,4,8,9,2] dom: <li>a</li> <li>b</li> <li>c</li> <li>f</li> <li>e</li> <li>d</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,f,e,d,g]
比較oldStartVal和newEndVal是否相等,若是相等則oldStartIdx和newEndIdx分別加1和減1,oldStartVal對應的vnode移動到oldEndVal對應的vnode後面,並對移動的vnode進行patch,進入下一次循環;這裏oldStartVal==newEndVal,因此oldStartIdx:2 newEndIdx:4;若不相等,繼續比較;
比較事後: oldStartIdx:2 oldEndIdx:5 oldStartVal:3 oldEndVal:6 newStartIdx:1 newEndIdx:4 newStartVal:6 newEndVal:9 比較範圍縮小後: oldKey:[3,4,5,6] newKey:[6,4,8,9] dom: <li>a</li> <li>c</li> <li>f</li> <li>e</li> <li>d</li> <li>b</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,f,e,d,g]
比較oldEndVal和newStartVal是否相等,若是相等則oldEndIdx和newStartIdx分別減1和加1,oldEndVal對應的vnode移動到oldStart對應的vnode前面,並對移動的vnode進行patch,進入下一次循環;這裏oldEndVal==newStartVal,因此oldEndIdx:4 newStartIdx:2;若不相等,繼續比較;
比較事後: oldStartIdx:2 oldEndIdx:4 oldStartVal:3 oldEndVal:5 newStartIdx:2 newEndIdx:4 newStartVal:4 newEndVal:9 比較範圍縮小後: oldKey:[3,4,5] newKey:[4,8,9] dom: <li>a</li> <li>d</li> <li>c</li> <li>f</li> <li>e</li> <li>b</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,f,e,d,g]
若不知足上述判斷條件,查找newStartVal對應的vnode是否存在於舊vnode數組中。若存在,移動這個舊的vnode到oldStartVal對應的vnode前面,並對這個移動的vnode進行patch,在舊的vnode數組中將其原來的位置置爲undefined,而且newStartIdx加1;
比較事後: oldStartIdx:2 oldEndIdx:4 oldStartVal:3 oldEndVal:5 newStartIdx:3 newEndIdx:4 newStartVal:8 newEndVal:9 比較範圍縮小後: oldKey:[3,4,5] newKey:[8,9] dom: <li>a</li> <li>d</li> <li>f</li> <li>c</li> <li>e</li> <li>b</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,undefined,e,d,g]
若不存在,則將這個newStartVal對應的vnde添加到oldStartVal對應的vnode前面,而且newStartIdx加1;
比較事後: oldStartIdx:2 oldEndIdx:4 oldStartVal:3 oldEndVal:5 newStartIdx:4 newEndIdx:4 newStartVal:9 newEndVal:9 比較範圍縮小後: oldKey:[3,4,5] newKey:[9] dom: <li>a</li> <li>d</li> <li>f</li> <li>h</li> <li>c</li> <li>e</li> <li>b</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,undefined,e,d,g] 這裏循環了兩次 比較事後: oldStartIdx:2 oldEndIdx:4 oldStartVal:3 oldEndVal:5 newStartIdx:5 newEndIdx:4 newStartVal:undefined newEndVal:9 比較範圍縮小後: oldKey:[3,4,5] newKey:[] dom: <li>a</li> <li>d</li> <li>f</li> <li>h</li> <li>k</li> <li>c</li> <li>e</li> <li>b</li> <li>g</li> oldVnodeArray:舊的vnode數組 [a,b,c,undefined,e,d,g]
循環結束,判斷新舊vnode的key值哪一個遍歷完,若是舊的便利完,若舊vnode數組遍歷完,則將剩餘的新vnode數組中的vnode進行添加;若新vnode數組遍歷完,則刪除剩餘的舊vnode數組中的vnode
在上面例子中,咱們須要刪除oldVnodeArray中的三個vnode,索引分別爲3,4,5,從而刪除了vnode對應的elm
最後獲得最終的dom結構
<ul> <li>a</li> <li>d</li> <li>f</li> <li>h</li> <li>k</li> <li>b</li> <li>g</li> </ul>
上面的例子沒有將全部狀況所有概括進來,不過應該包含了大部分狀況了。還須要注意的就是:
經過上面的分析,其實還能夠發現一個key值的特色,就是惟一性和一一對應性。惟一性好理解,畢竟key值就是用來每一個vnode本身的標示;一一對應表明着是你舊vnode和新vnode中若是沒有改變,則其key值應保持不變,之因此要提這個是由於不少地方看到了進行循環渲染的時候其key值都是用的數組的index進行賦值
若是考慮這種狀況 <li>a</li> <li>c</li> <li>b</li> --> <li>a</li> <li>c</li> <li>b</li> 通常這種dom結構都是放在數組裏面循環輸出的,若是它們的key值是按照index進行賦值的話,就須要這個地方須要進行三次dom操做,就是依次修改其節點的文本值; [a(1),b(2),c(3)] [c(1),a(2),b(3)] 那若是我使得它們改變前面對應的元素的key值不改變的,一一對應的話,這裏只須要一次dom操做,就是把<li>c</li>移動到最前面 [a(1),b(2),c(3)] [c(3),a(1),b(2)]