首先要說diff算法以前,仍是稍微解釋一下虛擬DOM,雖然大部分人都知道虛擬DOM的概念了。javascript
首先,不少人沒有意識到一個問題,現代前端框架爲咱們解決了什麼? 我認爲前端現代框架解決的是忽略對DOM的操做,讓前端人員注重於維護狀態。前端
對於視圖更新以往的解決方式是,不關心任何狀態,只須要將全部DOM刪掉,而後從新生成一份DOM,可是這種訪問DOM的方式會形成至關多的性能浪費。vue
而虛擬DOM在框架中的任務就是經過狀態生成相應結構的虛擬節點樹,使用新生成的虛擬節點樹和上次的虛擬節點樹進行對比,當某個狀態發生變化時,而後只更新和渲染不一樣的部分。java
在Vue1.0中採用了極高的細粒度,每個綁定一個對應的watcher實例,進行觀察狀態的變化,可是當狀態越多的節點被使用時,會有一些內存開銷以及一些依賴追蹤的開銷。node
在Vue2.0之後,引入了虛擬DOM,爲單個組件設置一個watcher實例,即便組件內有10個節點,裏面狀態發生變化時,只會通知到組件,而後組件內部經過虛擬DOM進行對比和渲染。算法
虛擬DOM的生成數組
咱們來結合以前《Vue不看源碼懂原理》系列——Vue模板編譯中提到的模板渲染結合起來看。Vue經過編譯將模板轉換AST,以後將AST轉換成渲染函數,執行渲染函數能夠獲得一個虛擬節點樹(vnode),再拿新生成的虛擬節點樹去和舊的虛擬節點樹(oldVnode)對比,找出更新部分節點,最後作渲染。 前端框架
科普完畢,接下來一段用來解釋上述中的vnode與oldVnode對比的過程,也就是你們常說的diff算法。app
diff算法解決的問題是否是暴力修改DOM,而是經過對象對DOM進行更新替換,通常包括三種主要邏輯:框架
前兩種都相對比較簡單,一個一個來講起。
新增節點最多見的場景就是,當oldVnode不存在這個節點,而vnode存在時,那麼表明它是一個全新的節點,須要使用vnode中的新節點去生成真實DOM插入到頁面的DOM中。
拿元素節點舉例,首先判斷它是否帶有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的內容便可。
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模板編譯
感謝點贊鼓勵,
也歡迎討論。