vue源碼淺談分析之Vue中的虛擬DOM理解

一、 虛擬DOM

  1. 什麼是虛擬DOM

    所謂虛擬DOM,就是用一個JS對象來描述一個DOM節點,像如下示例:

    <div class="a" id="b">我是內容</div>
    
    {
      tag:'div',        // 元素標籤
      attrs:{           // 屬性
        class:'a',
        id:'b'
      },
      text:'我是內容',  // 文本內容
      children:[]       // 子元素
    }
    

    我們把組成一個DOM節點的必要東西通過一個JS對象表示出來,那麼這個JS對象就可以用來描述這個DOM節點,我們把這個JS對象就稱爲是這個真實DOM節點的虛擬DOM節點。

  2. 爲什麼要有虛擬DOM

    我們知道,Vue是數據驅動視圖的,數據發生變化視圖就要隨之更新,在更新視圖的時候難免要操作DOM,而操作真實DOM又是非常耗費性能的,這是因爲瀏覽器的標準就把 DOM 設計的非常複雜,所以一個真正的 DOM 元素是非常龐大的,如下所示:

    let div = document.createElement('div')
    let str = ''
    for (const key in div) {
      str += key + ''
    }
    console.log(str)
    

真實的 DOM 節點數據會佔據更大的內存,當我們頻繁的去做 DOM 更新,會產生一定的性能問題,因爲 DOM 的更新有可能帶來頁面的重繪或重排。

那麼有沒有什麼解決方案呢?當然是有的。我們可以用 JS 的計算性能來換取操作 DOM 所消耗的性能。

既然我們逃不掉操作DOM這道坎,但是我們可以儘可能少的操作 DOM 。那如何在更新視圖的時候儘可能少的操作 DOM 呢?最直觀的思路就是我們不要盲目的去更新視圖,而是通過對比數據變化前後的狀態,計算出視圖中哪些地方需要更新,只更新需要更新的地方,而不需要更新的地方則不需關心,這樣我們就可以儘可能少的操作 DOM 了。這也就是上面所說的用 JS 的計算性能來換取操作 DOM 的性能。

我們可以用 JS 模擬出一個 DOM 節點,稱之爲虛擬 DOM 節點。當數據發生變化時,我們對比變化前後的虛擬DOM節點,通過DOM-Diff算法計算出需要更新的地方,然後去更新需要更新的視圖。

這就是虛擬 DOM 產生的原因以及最大的用途。

另外,使用虛擬 DOM 也能使得 Vue 不再依賴於瀏覽器環境。我們可以很容易的在 Broswer 端或者服務器端操作虛擬 DOM, 需要 render 時再將虛擬 DOM 轉換爲真實 DOM 即可。這也使得 Vue 有了實現服務器端渲染的能力。

二、 Vue中的虛擬DOM

  1. VNode

我們說了,虛擬DOM就是用JS來描述一個真實的DOM節點。而在Vue中就存在了一個VNode類,通過這個類,我們就可以實例化出不同類型的虛擬DOM節點,源碼如下:


export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag                                /*當前節點的標籤名*/
    this.data = data        /*當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,可以參考VNodeData類型中的數據信息*/
    this.children = children  /*當前節點的子節點,是一個數組*/
    this.text = text     /*當前節點的文本*/
    this.elm = elm       /*當前虛擬節點對應的真實dom節點*/
    this.ns = undefined            /*當前節點的名字空間*/
    this.context = context          /*當前組件節點對應的Vue實例*/
    this.fnContext = undefined       /*函數式組件對應的Vue實例*/
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key           /*節點的key屬性,被當作節點的標誌,用以優化*/
    this.componentOptions = componentOptions   /*組件的option選項*/
    this.componentInstance = undefined       /*當前節點對應的組件的實例*/
    this.parent = undefined           /*當前節點的父節點*/
    this.raw = false         /*簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false*/
    this.isStatic = false         /*靜態節點標誌*/
    this.isRootInsert = true      /*是否作爲跟節點插入*/
    this.isComment = false             /*是否爲註釋節點*/
    this.isCloned = false           /*是否爲克隆節點*/
    this.isOnce = false                /*是否有v-once指令*/
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  get child (): Component | void {
    return this.componentInstance
  }
}

從上面的代碼中可以看出:VNode類中包含了描述一個真實DOM節點所需要的一系列屬性,如tag表示節點的標籤名,text表示節點中包含的文本,children表示該節點包含的子節點等。通過屬性之間不同的搭配,就可以描述出各種類型的真實DOM節點。

  1. VNode的類型

通過屬性之間不同的搭配,VNode類可以描述出各種類型的真實DOM節點。那麼它都可以描述出哪些類型的節點呢?通過閱讀源碼,可以發現通過不同屬性的搭配,可以描述出以下幾種類型的節點。

  • 註釋節點
  • 文本節點
  • 元素節點
  • 組件節點
  • 函數式組件節點
  • 克隆節點

接下來,我們就把這幾種類型的節點描述方式從源碼中一一對應起來。

註釋節點

註釋節點描述起來相對就非常簡單了,它只需兩個屬性就夠了,源碼如下:

// 創建註釋節點
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

從上面代碼中可以看到,描述一個註釋節點只需兩個屬性,分別是:textisComment。其中text屬性表示具體的註釋信息,isComment是一個標誌,用來標識一個節點是否是註釋節點。

文本節點

文本節點描述起來比註釋節點更簡單,因爲它只需要一個屬性,那就是text屬性,用來表示具體的文本信息。源碼如下:

// 創建文本節點
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

克隆節點

克隆節點就是把一個已經存在的節點複製一份出來,它主要是爲了做模板編譯優化時使用,這個後面我們會說到。關於克隆節點的描述,源碼如下:

// 創建克隆節點
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

從上面代碼中可以看到,克隆節點就是把已有節點的屬性全部複製到新節點中,而現有節點和新克隆得到的節點之間唯一的不同就是克隆得到的節點isClonedtrue

元素節點

相比之下,元素節點更貼近於我們通常看到的真實DOM節點,它有描述節點標籤名詞的tag屬性,描述節點屬性如classattributes等的data屬性,有描述包含的子節點信息的children屬性等。由於元素節點所包含的情況相比而言比較複雜,源碼中沒有像前三種節點一樣直接寫死(當然也不可能寫死),那就舉個簡單例子說明一下:

// 真實DOM節點
<div id='a'><span>難涼熱血</span></div>

// VNode節點
{
  tag:'div',
  data:{},
  children:[
    {
      tag:'span',
      text:'難涼熱血'
    }
  ]
}

我們可以看到,真實DOM節點中:div標籤裏面包含了一個span標籤,而span標籤裏面有一段文本。反應到VNode節點上就如上所示:tag表示標籤名,data表示標籤的屬性id等,children表示子節點數組。

組件節點

組件節點除了有元素節點具有的屬性之外,它還有兩個特有的屬性:

  • componentOptions :組件的option選項,如組件的props
  • componentInstance :當前組件節點對應的Vue實例

函數式組件節點

函數式組件節點相較於組件節點,它又有兩個特有的屬性:

  • fnContext:函數式組件對應的Vue實例
  • fnOptions: 組件的option選項

小結

以上就是VNode可以描述的多種節點類型,它們本質上都是VNode類的實例,只是在實例化的時候傳入的屬性參數不同而已。

  1. VNode的作用

說了這麼多,那麼VNodeVue的整個虛擬DOM過程起了什麼作用呢?

其實VNode的作用是相當大的。我們在視圖渲染之前,把寫好的template模板先編譯成VNode並緩存下來,等到數據發生變化頁面需要重新渲染的時候,我們把數據發生變化後生成的VNode與前一次緩存下來的VNode進行對比,找出差異,然後有差異的VNode對應的真實DOM節點就是需要重新渲染的節點,最後根據有差異的VNode創建出真實的DOM節點再插入到視圖中,最終完成一次視圖更新。

  1. 總結

虛擬DOM就是以JS的計算性能來換取操作真實DOM所消耗的性能,在Vue中是通過VNode類來實例化出不同類型的虛擬DOM節點,並且學習了不同類型節點生成的屬性的不同,所謂不同類型的節點其本質還是一樣的,都是VNode類的實例,只是在實例化時傳入的屬性參數不同罷了。最後探究了VNode的作用,有了數據變化前後的VNode,我們才能進行後續的DOM-Diff找出差異,最終做到只更新有差異的視圖,從而達到儘可能少的操作真實DOM的目的,以節省性能。

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