vue源碼(五)-vue虛擬dom和diff對比

vue源碼(五)-vue虛擬dom和diff對比

一、虛擬DOM

1、概念

虛擬DOM(Virtual DOM)是對DOM的JS抽象表示,它們是JS對象,能夠描述DOM結構和關係。應用程序 的各種狀態變化會作用於虛擬DOM,最終映射到DOM上。

2、優點

虛擬DOM輕量、快速:當它們發生變化時通過新舊虛擬DOM比對可以得到最小DOM操作量,從 而提升性能和用戶體驗。
跨平臺:將虛擬dom更新轉換爲不同運行時特殊操作實現跨平臺
兼容性:還可以加入兼容性代碼增強操作的兼容性

3、必要性

vue 1.0中有細粒度的數據變化偵測,它是不需要虛擬DOM的,但是細粒度造成了大量開銷,這對於大 型項目來說是不可接受的。因此,vue 2.0選擇了中等粒度的解決方案,每一個組件一個watcher實例, 這樣狀態變化時只能通知到組件,再通過引入虛擬DOM去進行比對和渲染。

4、源碼實現

通過文章vue源碼(四)-vue項目配置和入口文件,數據響應化處理分析發現,有虛擬dom掛載真實dom是由new Vue() => _init() => $mount()這樣一個執行流程進行的。而$mount最終的實現代碼來源於src/core/instance/lifecycle.js文件的mountComponent

查看該方法,發現196-202行處理了updateComponent方法,updateComponent的定義是189行。

updateComponent方法中發現調用順序是_render然後是_update。 其中_render就是生成虛擬dom,然後由_update更新到真實dom中。繼續查找_render方法,打開文件src/core/instance/render.js,查看_render定義,可以看到83行定義了vnode,在89行通過傳遞參數vm.$createElement調用render方法生成虛擬dom。

再來查看文件src/core/vdom/create-element.js,99行開始進行虛擬dom的創建,首先對傳入的標籤tag進行判斷,由於創建時會存在原生標籤和自定義標籤兩種情況,所以通過判斷是否爲字符串進行處理。具體如下所示實現:

當tag爲字符串的時,判斷是否爲原生標籤,通過new Node()方式進行創建虛擬dom,如果不是原生標籤,則進行判斷是否爲自定義組件,按照自定義組件進行處理,當以上條件不符合時,直接通過createComponent進行創建虛擬dom。查看src/core/vdom/create-component.js文件中createComponent方法,最終仍然是返回通過new VNode()創建的虛擬dom。
下面我們使用真實代碼生成虛擬dom打印查看下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>Vue.js elastic header example</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      {{message}}
    </div>
    <script>
      new Vue({
        el: '#app',
        data: {
          message: 'hello world!初始化流程和入口文件!'
        }
      })
    </script>
  </body>
</html>

最終生成虛擬dom如下:

二、diff對比

生成的虛擬dom,最終是需要更新到真實dom中的,從之前代碼查看過程中,我們能夠知道最終的diff位於文件core\vdom\patch.js,通過調用方法createPatchFunction進行渲染。

  1. patch中通過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,所以時間複雜度只 有O(n),是一種相當高效的算法。
  2. 同層級只做三件事:增刪改。具體規則是:new VNode不存在就刪;old VNode不存在就增;都存在就 比較類型,類型不同直接替換、類型相同執行更新;

該文件中方法patchVnode進行執行更新操作:
兩個VNode類型相同,就執行更新操作,包括三種類型操作:屬性更新PROPS、文本更新TEXT、子節點更新REORDER
patchVnode具體規則如下:

  1. 如果新舊VNode都是靜態的,那麼只需要替換elm以及componentInstance即可。
  2. 新老節點均有children子節點,則對子節點進行diff操作,調用updateChildren
  3. 如果老節點沒有子節點而新節點存在子節點,先清空老節點DOM的文本內容,然後爲當前DOM節
    點加入子節點。
  4. 當新節點沒有子節點而老節點有子節點的時候,則移除該DOM節點的所有子節點。
  5. 當新老節點都無子節點的時候,只是文本的替換。

文件中updateChildren 主要作用是用一種較高效的方式比對新舊兩個VNode的children得出最小操作補丁。執行一個雙循環是傳統方式,vue中針對web場景特點做了特別的算法優化

在新老兩組VNode節點的左右頭尾兩側都有一個變量標記,在遍歷過程中這幾個變量都會向中間靠攏。 當oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx時結束循環。
下面是遍歷規則:
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩交叉比較,共有4種比較 方法。

  • 當 oldStartVnode和newStartVnode 或者 oldEndVnode和newEndVnode 滿足sameVnode,直接將該 VNode節點進行patchVnode即可,不需再遍歷就完成了一次循環。
  • 如果oldStartVnode與newEndVnode滿足sameVnode。說明oldStartVnode已經跑到了oldEndVnode 後面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的後面。
  • 如果oldStartVnode與newEndVnode滿足sameVnode。說明oldStartVnode已經跑到了oldEndVnode 後面去了,進行patchVnode的同時還需要將真實DOM節點移動到oldEndVnode的後面。
  • 如果oldEndVnode與newStartVnode滿足sameVnode,說明oldEndVnode跑到了oldStartVnode的前 面,進行patchVnode的同時要將oldEndVnode對應DOM移動到oldStartVnode對應DOM的前面。

如果以上情況均不符合,則在old VNode中找與newStartVnode滿足sameVnode的vnodeToMove,若 存在執行patchVnode,同時將vnodeToMove對應DOM移動到oldStartVnode對應的DOM的前面。當然也有可能newStartVnode在old VNode節點中找不到一致的key,或者是即便key相同卻不是 sameVnode,這個時候會調用createElm創建一個新的DOM節點。459行是執行createElm過程。

至此循環結束,但是我們還需要處理剩下的節點。
當結束時oldStartIdx > oldEndIdx,這個時候舊的VNode節點已經遍歷完了,但是新的節點還沒有。說明了新的VNode節點實際上比老的VNode節點多,需要將剩下的VNode對應的DOM插入到真實DOM 中,此時調用addVnodes(批量調用createElm接口)。
但是,當結束時newStartIdx > newEndIdx時,說明新的VNode節點已經遍歷完了,但是老的節點還有 剩餘,需要從文檔中刪 的節點刪除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章