本文章是轉載的,爲了方便之後複習,特意記錄一下。他人請去原地址觀看!!!
文章原地址:https://blog.csdn.net/m6i37jk/article/details/78140159
做者簡介:汪玉林,高級工程師,增值產品部前端Leader,目前團隊負責手Q遊戲中心、手Q遊戲運營、手Q閱讀等項目,有豐富的Web前端架構經驗。 html
Vue的核心是雙向綁定和虛擬DOM(下文咱們簡稱爲vdom),關於雙向綁定能夠參閱木琴的文章《剖析Vue原理&實現雙向綁定MVVM》,vdom是樹狀結構,其節點爲vnode,vnode和瀏覽器DOM中的Node一一對應,經過vnode的elm屬性能夠訪問到對應的Node。
前端
vdom由於是純粹的JS對象,因此操做它會很高效,可是vdom的變動最終會轉換成DOM操做,爲了實現高效的DOM操做,一套高效的虛擬DOM diff算法顯得頗有必要。vue
Vue的diff算法是基於snabbdom改造過來的,感興趣的朋友能夠選擇查閱。node
這是一張很經典的圖,出自《React’s diff algorithm》,Vue的diff算法也一樣,即僅在同級的vnode間作diff,遞歸地進行同級vnode的diff,最終實現整個DOM樹的更新。那同級vnode diff的細節又是怎樣的呢?正是本文所要講的。算法
咱們在下文中將使用這個簡化的例子來說述diff的過程瀏覽器
如上圖的例子,更新前是1到10排列的Node列表,更新後是亂序排列的Node列表。羅列一下圖中有如下幾種類型的節點變化狀況:架構
(1)、頭部相同、尾部相同的節點:如一、10dom
(2)、頭尾相同的節點:如二、9(處理完頭部相同、尾部相同節點以後)ide
(3)、新增的節點:11性能
(4)、刪除的節點:8
(5)、其餘節點:三、四、五、六、7
簡單的diff算法能夠這樣設計:
逐個遍歷newVdom的節點,找到它在oldVdom中的位置,若是找到了就移動對應的DOM元素,若是沒找到說明是新增節點,則新建一個節點插入。遍歷完成以後若是oldVdom中還有沒處理過的節點,則說明這些節點在newVdom中被刪除了,刪除它們便可。
仔細思考一下,幾乎每一步都要作移動DOM的操做,這在DOM總體結構變化不大時的開銷是很大的,實際上DOM變化不大的狀況現實中常常發生,不少時候咱們只須要變動某個節點的文本而已。
接下來咱們看一下Vue的diff實現
上圖例子中我畫上了oldStart+oldEnd,newStart+newEnd這樣2對指針,分別對應oldVdom和newVdom的起點和終點。起止點以前的節點是待處理的節點,Vue不斷對vnode進行處理同時移動指針直到其中任意一對起點和終點相遇。處理過的節點Vue會在oldVdom和newVdom中同時將它標記爲已處理(標記方法後文中有介紹)。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-管理可複用的元素
先看一張總體視圖,整個diff分兩部分:
(1)、第一部分是一個循環,循環內部是一個分支邏輯,每次循環只會進入其中的一個分支,每次循環會處理一個節點,處理以後將節點標記爲已處理(oldVdom和newVdom都要進行標記,若是節點只出如今其中某一個vdom中,則另外一個vdom中不須要進行標記),標記的方法有2種,當節點正好在vdom的指針處,移動指針將它排除到未處理列表以外便可,不然就要採用其餘方法,Vue的作法是將節點設置爲undefined。
(2)、循環結束以後,可能newVdom或者oldVdom中還有未處理的節點,若是是newVdom中有未處理節點,則這些節點是新增節點,作新增處理。若是是oldVdom中有這類節點,則這些是須要刪除的節點,相應在DOM樹中刪除之
整個過程是逐步找到更新先後vdom的差別,而後將差別反應到DOM樹上(也就是patch),特別要提一下Vue的patch是即時的,並非打包全部修改最後一塊兒操做DOM(React則是將更新放入隊列後集中處理),朋友們會問這樣作性能不好吧?實際上現代瀏覽器對這樣的DOM操做作了優化,並沒有差異。
(1)、處理頭部的同類型節點,即oldStart和newStart指向同類節點的狀況,以下圖中的節點1
這種狀況下,將節點1的變動更新到DOM,而後對其進行標記,標記方法是oldStart和newStart後移1位便可,過程當中不須要移動DOM(更新DOM或許是要的,好比屬性變動了,文本內容變動了等等)
(2)、處理尾部的同類型節點,即oldEnd和newEnd指向同類節點的狀況,以下圖中的節點10
與狀況(1)相似,這種狀況下,將節點10的變動更新到DOM,而後oldEnd和newEnd前移1位進行標記,一樣也不須要移動DOM
(3)、處理頭尾/尾頭的同類型節點,即oldStart和newEnd,以及oldEnd和newStart指向同類節點的狀況,以下圖中的節點2和節點9
先看節點2,實際上是日後移了,移到哪裏?移到oldEnd指向的節點(即節點9)後面,移動以後標記該節點,將oldStart後移1位,newEnd前移一位
操做結束以後狀況以下圖
一樣地,節點9也是相似的處理,處理完以後成了下面這樣
(4)、處理新增的節點
newStart來到了節點11的位置,在oldVdom中找不到節點11,說明它是新增的
那麼就建立一個新的節點,插入DOM樹,插到什麼位置?插到oldStart指向的節點(即節點3)前面,而後將newStart後移1位標記爲已處理(注意oldVdom中沒有節點11,因此標記過程當中它的指針不須要移動),處理以後以下圖
(5)、處理更新的節點
通過第(4)步以後,newStart來到了節點7的位置,在oldVdom中能找到它並且不在指針位置(查找oldVdom中oldStart到oldEnd區間內的節點),說明它的位置移動了
那麼須要在DOM樹中移動它,移到哪裏?移到oldStart指向的節點(即節點3)前面,與此同時將節點標記爲已處理,跟前面幾種狀況有點不一樣,newVdom中該節點在指針下,能夠移動newStart進行標記,而在oldVdom中該節點不在指針處,因此採用設置爲undefined的方式來標記(必定要標記嗎?後面會提到)
處理以後就成了下面這樣
(6)、處理三、四、五、6節點
通過第(5)步處理以後,咱們看到了使人欣慰的一幕,newStart和oldStart又指向了同一個節點(即都指向節點3),很簡單,按照(1)中的作法只需移動指針便可,很是高效,三、四、五、6都如此處理,處理完以後以下圖
(7)、處理需刪除的節點
通過前6步處理以後(實際上前6步是循環進行的),朋友們看newStart跨過了newEnd,它們相遇啦!而這個時候,oldStart和oldEnd尚未相遇,說明這2個指針之間的節點(包括它們指向的節點,即上圖中的節點七、節點8)是這次更新中被刪掉的節點。
OK,那咱們在DOM樹中將它們刪除,再回到前面咱們對節點7作了標記,爲何標記是必需的?標記的目的是告訴Vue它已經處理過了,是須要出如今新DOM中的節點,不要刪除它,因此在這裏只需刪除節點8。
在應用中也可能會遇到oldVdom的起止點相遇了,可是newVdom的起止點沒有相遇的狀況,這個時候須要對newVdom中的未處理節點進行處理,這類節點屬於更新中被加入的節點,須要將他們插入到DOM樹中。
至此,整個diff過程結束了
Vue的diff算法與動態規劃算法中的經典案例「計算a到b的最小編輯距離」看上去有些類似,實際徹底不一樣,Vue的diff相對來講輕量不少,感興趣的朋友能夠查閱相關資料進行了解。
好啦,感謝你的閱讀,但願能幫助你理解Vue的diff算法,在閱讀過程當中遇到的問題也歡迎一塊兒交流!