vue中的diff算法

一、前言

Vue的核心是雙向綁定和虛擬DOM,vdom是樹狀結構,其節點爲vnode,vnode和瀏覽器DOM中的Node一一對應,通過vnode的elm屬性可以訪問到對應的Node。

vdom因爲是純粹的JS對象,所以操作它會很高效,但是vdom的變更最終會轉換成DOM操作,爲了實現高效的DOM操作,一套高效的虛擬DOM diff算法顯得很有必要。

640?wx_fmt=png&wxfrom=5&wx_lazy=1

  • 僅在同級的vnode間做diff,遞歸地進行同級vnode的diff,最終實現整個DOM樹的更新。

二、舉例

在下文中將使用這個簡化的例子來講述diff的過程:

0?wx_fmt=png

如上圖的例子,更新前是1到10排列的Node列表,更新後是亂序排列的Node列表。羅列一下圖中有以下幾種類型的節點變化情況:

  1. 頭部相同、尾部相同的節點:如1、10

  2. 頭尾相同的節點:如2、9(處理完頭部相同、尾部相同節點之後)

  3. 新增的節點:11

  4. 刪除的節點:8

  5. 其他節點:3、4、5、6、7

三、diff

diff算法可以這樣設計:

  • 逐個遍歷newVdom的節點,找到它在oldVdom中的位置,如果找到了就移動對應的DOM元素,如果沒找到說明是新增節點,則新建一個節點插入。遍歷完成之後如果oldVdom中還有沒處理過的節點,則說明這些節點在newVdom中被刪除了,刪除它們即可。

四、Vue的diff實現

  1. 上圖例子中有oldStart+oldEnd,newStart+newEnd兩對指針,分別對應oldVdom和newVdom的起點和終點。起止點之前的節點是待處理的節點,Vue不斷對vnode進行處理同時移動指針直到其中任意一對起點和終點相遇。處理過的節點Vue會在oldVdom和newVdom中同時將它標記爲已處理(標記方法後文中有介紹)

  2. Vue通過以下措施來提升diff的性能。

(一)優先處理特殊場景

(1)頭部的同類型節點、尾部的同類型節點

這類節點更新前後位置沒有發生變化,所以不用移動它們對應的DOM

(2)頭尾/尾頭的同類型節點

這類節點位置很明確,不需要再花心思查找,直接移動DOM就好

處理了這些場景之後,一方面一些不需要做移動的DOM得到快速處理,另一方面待處理節點變少,縮小了後續操作的處理範圍,性能也得到提升

(二)“原地複用”
“原地複用”是指Vue會儘可能複用DOM,儘可能不發生DOM的移動。Vue在判斷更新前後指針是否指向同一個節點,其實不要求它們真實引用同一個DOM節點,實際上它僅判斷指向的是否是同類節點(比如2個不同的div,在DOM上它們是不一樣的,但是它們屬於同類節點),如果是同類節點,那麼Vue會直接複用DOM,這樣的好處是不需要移動DOM。再看上面的實例,假如10個節點都是div,那麼整個diff過程中就沒有移動DOM的操作了。

“原地複用”在Vue的官方文檔中有提到,雖然帶來了好處,但是也會產生一些問題:

https://cn.vuejs.org/v2/guide/list.html#key

https://cn.vuejs.org/v2/guide/conditional.html#用-key-管理可複用的元素

五、按步解剖實例

(一)整體視圖
0?wx_fmt=png

先看一張整體視圖,整個diff分兩部分:

(1)第一部分是一個循環,循環內部是一個分支邏輯,每次循環只會進入其中的一個分支,每次循環會處理一個節點,處理之後將節點標記爲已處理(oldVdom和newVdom都要進行標記,如果節點只出現在其中某一個vdom中,則另一個vdom中不需要進行標記)。
標記的方法有兩種,當節點正好在vdom的指針處,移動指針將它排除到未處理列表之外即可,否則就要採用其他方法,Vue的做法是將節點設置爲undefined。

(2)循環結束之後,可能newVdom或者oldVdom中還有未處理的節點,如果是newVdom中有未處理節點,則這些節點是新增節點,做新增處理;如果是oldVdom中有這類節點,則這些是需要刪除的節點,相應在DOM樹中刪除之。

  • 整個過程是逐步找到更新前後vdom的差異,然後將差異反應到DOM樹上(也就是patch),特別要提一下Vue的patch是即時的,並不是打包所有修改最後一起操作DOM(React則是將更新放入隊列後集中處理)。

(二)逐步解析

(1)處理頭部的同類型節點,即oldStart和newStart指向同類節點的情況,如下圖中的節點1

  • 將節點1的變更更新到DOM,然後對其進行標記,標記方法是oldStart和newStart後移1位,過程中不需要移動DOM(更新DOM或許是要的,比如屬性變更,文本內容變更等)

0?wx_fmt=png

(2)處理尾部的同類型節點,即oldEnd和newEnd指向同類節點的情況,如下圖中的節點10

  • 與情況(1)類似,將節點10的變更更新到DOM,然後oldEnd和newEnd前移1位進行標記,同樣也不需要移動DOM

0?wx_fmt=png

(3)處理頭尾/尾頭的同類型節點,即oldStart和newEnd,以及oldEnd和newStart指向同類節點的情況,如下圖中的節點2和節點9

  • 先看節點2,其實是往後移了,移到oldEnd指向的節點(即節點9)後面,移動之後標記該節點,將oldStart後移1位,newEnd前移一位

0?wx_fmt=png

操作結束之後情況如下圖:

0?wx_fmt=png

  • 同樣地,節點9也是類似的處理:

0?wx_fmt=png

(4)處理新增的節點

  • newStart來到了節點11的位置,在oldVdom中找不到節點11,說明它是新增的

創建一個新的節點,插入DOM樹,插到oldStart指向的節點(即節點3)前面,然後將newStart後移1位標記爲已處理(注意oldVdom中沒有節點11,所以標記過程中它的指針不需要移動)

處理之後如下圖:

0?wx_fmt=png

(5)處理更新的節點

  • 經過第(4)步之後,newStart來到了節點7的位置,在oldVdom中能找到它而且不在指針位置(查找oldVdom中oldStart到oldEnd區間內的節點),說明它的位置移動了。

  • 需要在DOM樹中移動它,移到哪裏?移到oldStart指向的節點(即節點3)前面,與此同時將節點標記爲已處理,跟前面幾種情況有點不同,newVdom中該節點在指針下,可以移動newStart進行標記,而在oldVdom中該節點不在指針處,所以採用設置爲undefined的方式來標記。

0?wx_fmt=png

處理之後的結果:

0?wx_fmt=png

(6)處理3、4、5、6節點

  • 經過第(5)步處理之後,newStart和oldStart又指向了同一個節點(即都指向節點3),按照(1)中的做法只需移動指針即可,非常高效,3、4、5、6都如此處理

處理完之後如下圖:

0?wx_fmt=png

(7)處理需刪除的節點

  • 經過前6步處理之後(實際上前6步是循環進行的),newStart跨過了newEnd,這個時候,oldStart和oldEnd還沒有相遇,說明這2個指針之間的節點(包括它們指向的節點,即上圖中的節點7、節點8)是此次更新中被刪掉的節點。

  • 在DOM樹中將它們刪除,再回到前面我們對節點7做了標記,爲什麼標記是必需的?

標記的目的是告訴Vue它已經處理過了,是需要出現在新DOM中的節點,不要刪除它,所以在這裏只需刪除節點8。

  • 在應用中也可能會遇到oldVdom的起止點相遇了,但是newVdom的起止點沒有相遇的情況,這時需要對newVdom中的未處理節點進行處理,這類節點屬於更新中被加入的節點,需要將他們插入到DOM樹中。

在這裏插入圖片描述

  • 至此,整個diff過程結束。

原文地址:https://blog.csdn.net/M6i37JK/article/details/78140159

發佈了152 篇原創文章 · 獲贊 25 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章