《Vue不看源碼懂原理》系列——Vue的diff算法不難懂

虛擬DOM

首先要說diff算法以前,仍是稍微解釋一下虛擬DOM,雖然大部分人都知道虛擬DOM的概念了。javascript

首先,不少人沒有意識到一個問題,現代前端框架爲咱們解決了什麼? 我認爲前端現代框架解決的是忽略對DOM的操做,讓前端人員注重於維護狀態。前端

對於視圖更新以往的解決方式是,不關心任何狀態,只須要將全部DOM刪掉,而後從新生成一份DOM,可是這種訪問DOM的方式會形成至關多的性能浪費。vue

而虛擬DOM在框架中的任務就是經過狀態生成相應結構的虛擬節點樹,使用新生成的虛擬節點樹和上次的虛擬節點樹進行對比,當某個狀態發生變化時,而後只更新和渲染不一樣的部分。java

在這裏插入圖片描述
Vue引入虛擬DOM的緣由

在Vue1.0中採用了極高的細粒度,每個綁定一個對應的watcher實例,進行觀察狀態的變化,可是當狀態越多的節點被使用時,會有一些內存開銷以及一些依賴追蹤的開銷。node

在Vue2.0之後,引入了虛擬DOM,爲單個組件設置一個watcher實例,即便組件內有10個節點,裏面狀態發生變化時,只會通知到組件,而後組件內部經過虛擬DOM進行對比和渲染。算法

虛擬DOM的生成數組

咱們來結合以前《Vue不看源碼懂原理》系列——Vue模板編譯中提到的模板渲染結合起來看。Vue經過編譯將模板轉換AST,以後將AST轉換成渲染函數,執行渲染函數能夠獲得一個虛擬節點樹(vnode),再拿新生成的虛擬節點樹去和舊的虛擬節點樹(oldVnode)對比,找出更新部分節點,最後作渲染。 前端框架

在這裏插入圖片描述
vnode和AST的區別 看過上一篇文章的應該知道AST的概念,那vnode並非AST,它是經過AST生成的虛擬DOM的節點,它是虛擬DOM的組成部分,經過AST生成的一個javascript對象版本的HTML結構,它和AST同樣是一種節點描述對象,裏面放了節點的全部信息。

科普完畢,接下來一段用來解釋上述中的vnode與oldVnode對比的過程,也就是你們常說的diff算法。app

diff算法

diff算法解決的問題是否是暴力修改DOM,而是經過對象對DOM進行更新替換,通常包括三種主要邏輯:框架

  1. 建立新增節點
  2. 刪除廢棄節點
  3. 修改須要更新的節點

前兩種都相對比較簡單,一個一個來講起。

建立新增節點

新增節點最多見的場景就是,當oldVnode不存在這個節點,而vnode存在時,那麼表明它是一個全新的節點,須要使用vnode中的新節點去生成真實DOM插入到頁面的DOM中。

在這裏插入圖片描述
除此以外還存在一種狀況,當新的vnode和oldVnode徹底不是一個節點時,要知道咱們是以vnode來渲染新的視圖,所以能夠得知新的vnode是一個全新的節點,而oldVnode是一個被廢棄的節點。這種狀況下咱們要作的事是建立一個vnode對應的新的DOM節點,去替換掉以前的DOM節點。
在這裏插入圖片描述
只有3種類型的節點會被建立和插入到DOM中分別對應AST中的元素節點,註釋節點,文本節點。

拿元素節點舉例,首先判斷它是否帶有tag屬性,若是有那麼它就是一個元素節點,調用對應的appendChild方法,將該節點插入到指定的父節點中。須要注意的是建立子節點是一個遞歸過程,就像你遍歷一個對象樹同樣,咱們須要將vnode的chaildren屬性進行循環一遍,爲每一個父節點的子節點也執行一遍建立節點的邏輯。

在這裏插入圖片描述

刪除節點

刪除節點的場景比較簡單,即上面說到的當存在一個被廢棄的節點時,咱們除了要插入新的替換節點,也要刪除以前的DOM節點。

刪除節點的實現邏輯以下:

function removeVnodes(vnodes,startIdx,endIdx){
    for(;startIdx<=endIdx;++startIdx){
      const ch = vnodes[startIdx]
      if(isDef(ch)){
        removeVnode(ch.elm) // 刪除單個節點方法
      }
    }
  }
複製代碼

刪除節點的邏輯就是刪除vnodes數組中從startIdx指定位置到endIdx的內容便可。

更新節點

  1. 在更新節點時,咱們首先須要判斷兩個虛擬節點是不是靜態節點,若是是,則直接跳過更新過程。
  2. 若是不是,且兩個節點有不一樣屬性時,要以新的vnode爲標準進行渲染更新。當節點爲text屬性時,name不論以前子節點內容是什麼,直接調用textContent方法將視圖中的DOM節點改爲新的vnode所保存的文字。 43 若是沒有text屬性,那麼他必定是一個元素節點(不理解的能夠參考上一篇中的AST類型《Vue不看源碼懂原理》系列——Vue模板編譯)。咱們再將更新節點分爲兩種:1有children。當新的vnode中存在children屬性時,咱們要先看oldVnode中是否也存在children屬性。若是oldVnode中也存在children屬性,那麼咱們要對各個children進行更詳細的遞歸對比。2沒有children。若是一個新建立的節點沒有text屬性也沒有children屬性時,那麼說明這新建立的節點是一個空節點,這時候若是oldVnode中有子節點,就執行子節點的刪除操做,有文本就執行文本的刪除操做,最後完成視圖中空標籤的效果。

子節點更新策略

Vue更新子節點的策略基於在比對子節點數組的時候,將接收的參數oldVnode的子節點構成的數組和nvnode的子節點構成的數組進行比較。

兩個數組做比較只須要一個雙層循環就搞定了,例如如今對oldVnode數組的第一個元素作判斷,我要拿着這個元素去和vnode裏面的元素一個個比過去,假設在對比到vnode中第三個元素的時候發現連個元素同樣,則表示oldVNode數組的第一個元素的位置發生了變化,在新數組中它變到了第三的位置。此時咱們能夠知道ldVnode數組的第一個元素位置變成了第三。    上面這種方式惟一存在的問題是效率過低。假設oldVnode和vnode有100個子元素,當咱們在比較oldVnode的最後一個元素的時候,發現它和vnode中的最後一個元素相同,這其實浪費了不少的計算資源。   所以vue對子節點更新進行了策略優化,Vue爲oldVnode和vnode分別添加了一對遊標,默認指向數組的第一個和最後一個元素,它實現的是一種從兩邊向中間查找的一種方式,全量查找至少在時間複雜度上減小了一倍。   

在這裏插入圖片描述

  • 若是oldStartIdx指向的元素爲undefined則oldStartIdx右移,一樣的若是oldEndIdx指向的元素不存在則oldEndIdx左移。這個操做的目的是快速去掉vnode左右兩端的無效數據。爲何會出現元素值爲undefined呢?往下看就知道了。
  • 若是oldStartIdx和newStartIdx是相同元素則對其調用patchVnode。oldStartIdx和newStartIdx都向右移動。 一樣的,若是newEndIdx和oldEndIdx是相同元素對其調用patchVNode。newEndIdx和oldEndIdx都向左移動。咱們認爲不少時候節點變化先後它的子節點數組的首尾元素還是相同元素。
  • 若是oldStartIdx和newEndIdx是相同元素則對其調用patchVnode,oldStartIdx右移,newEndIdx左移。若是oldEndIdx和newStartIdx是相同元素則對其調用patchVnode,oldEndIdx左移,newStartIdx右移。

diff的核心是遞歸比較子節點

正常Diff兩個樹的時間複雜度是O(n * 3),但實際狀況下咱們不多會進行跨層級的移動DOM,因此Vue將Diff進行了優化,從O(n * 3)> -> O(n),只有當新舊children都爲多個子節點時才須要用核心的Diff算法進行同層級比較。 Vue2的核心Diff算法採用了雙端比較的算法,同時重新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操做。相比React的Diff算法,一樣狀況下能夠減小移動節點次數,減小沒必要要的性能損耗,更加的優雅。

key在虛擬DOM的做用

新舊 children 中的節點只有順序是不一樣的時候,最佳的操做應該是經過移動元素的位置來達到更新的目的。 須要在新舊 children 的節點中保存映射關係,以便可以在舊children的節點中找到可複用的節點。key也就是children中節點的惟一標識。

到這呢就算把Vue中的節點更新過程就簡單講述一遍,可能有些邏輯講述的通常,文筆有限。

最好能夠結合上一篇一塊兒看《Vue不看源碼懂原理》系列——Vue模板編譯

感謝點贊鼓勵,

也歡迎討論。

相關文章
相關標籤/搜索