一、 虛擬DOM
-
什麼是虛擬
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
節點。 -
爲什麼要有虛擬
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
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
節點。
VNode
的類型
通過屬性之間不同的搭配,VNode
類可以描述出各種類型的真實DOM
節點。那麼它都可以描述出哪些類型的節點呢?通過閱讀源碼,可以發現通過不同屬性的搭配,可以描述出以下幾種類型的節點。
- 註釋節點
- 文本節點
- 元素節點
- 組件節點
- 函數式組件節點
- 克隆節點
接下來,我們就把這幾種類型的節點描述方式從源碼中一一對應起來。
註釋節點
註釋節點描述起來相對就非常簡單了,它只需兩個屬性就夠了,源碼如下:
// 創建註釋節點
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
從上面代碼中可以看到,描述一個註釋節點只需兩個屬性,分別是:text
和isComment
。其中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
}
從上面代碼中可以看到,克隆節點就是把已有節點的屬性全部複製到新節點中,而現有節點和新克隆得到的節點之間唯一的不同就是克隆得到的節點isCloned
爲true
。
元素節點
相比之下,元素節點更貼近於我們通常看到的真實DOM
節點,它有描述節點標籤名詞的tag
屬性,描述節點屬性如class
、attributes
等的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
類的實例,只是在實例化的時候傳入的屬性參數不同而已。
VNode
的作用
說了這麼多,那麼VNode
在Vue
的整個虛擬DOM
過程起了什麼作用呢?
其實VNode
的作用是相當大的。我們在視圖渲染之前,把寫好的template
模板先編譯成VNode
並緩存下來,等到數據發生變化頁面需要重新渲染的時候,我們把數據發生變化後生成的VNode
與前一次緩存下來的VNode
進行對比,找出差異,然後有差異的VNode
對應的真實DOM
節點就是需要重新渲染的節點,最後根據有差異的VNode
創建出真實的DOM
節點再插入到視圖中,最終完成一次視圖更新。
- 總結
虛擬DOM
就是以JS
的計算性能來換取操作真實DOM
所消耗的性能,在Vue
中是通過VNode
類來實例化出不同類型的虛擬DOM
節點,並且學習了不同類型節點生成的屬性的不同,所謂不同類型的節點其本質還是一樣的,都是VNode
類的實例,只是在實例化時傳入的屬性參數不同罷了。最後探究了VNode
的作用,有了數據變化前後的VNode
,我們才能進行後續的DOM-Diff
找出差異,最終做到只更新有差異的視圖,從而達到儘可能少的操作真實DOM
的目的,以節省性能。