其餘章節請看:javascript
vue 快速入門 系列html
dom 是文檔對象模型,以節點樹的形式來表現文檔。vue
虛擬 dom 不是真正意義上的 dom。而是一個 javascript 對象。java
正常的 dom 節點在 html 中是這樣表示:node
<div class='testId'> <p>你好</p> <p>歡迎光臨</p> </div>
而在虛擬 dom 中大概是這樣:app
{ tag: 'div', attributes:{ class: ['testId'] }, children:[ // p 元素 // p 元素 ] }
咱們能夠將虛擬 dom 拆分紅兩部分進行理解:虛擬 + dom。框架
前文(初步認識 vue)提到,如今主流的框架都是聲明式操做 dom 的框架。咱們只須要描述狀態與 dom 之間的映射關係便可,狀態到視圖(真實的 dom)的轉換,框架會幫咱們作。dom
最粗暴的作法是將狀態渲染成視圖,每次更新狀態,都從新更新整個視圖。async
這種作法的性能可想而知。比較好的想法是:狀態改變,只更新與狀態相關的 dom 節點。虛擬 dom 只是實現這個想法的其中一種方法而已。源碼分析
具體作法:
- 狀態 -> 真實 dom(最初)
- 狀態 -> 虛擬 dom -> 真實 dom(使用虛擬 dom)
狀態改變,從新生成一份虛擬 dom,將上一份和這一份虛擬 dom 進行對比,找出須要更新的部分,更新真實 dom。
真實的 dom 是由 節點(Node)組成,虛擬 dom 則是由虛擬節點(vNode)組成。
虛擬 dom 在 vue 中主要作兩件事:
「虛擬 DOM」是咱們對由 Vue 組件樹創建起來的整個 VNode 樹的稱呼 —— vue 官網
上文提到,vNode(虛擬節點)對應的是真實節點(Node)。
vNode 能夠理解成節點描述對象。描述瞭如何建立真實的 dom 節點。
vue.js 中有一個 vNode 類。可使用它建立不一樣類型的 vNode 實例,不一樣類型的 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 this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } get child (): Component | void { return this.componentInstance } }
從代碼不難看出 vNode 類建立的實例,本質上就是一個普通的 javascript 對象。
前面咱們已經介紹經過 vNode 類能夠建立不一樣類型的 vNode。而不一樣類型的 vNode 是由有效屬性區分。例如 isComment = true
表示註釋節點;isCloned = true
表示克隆節點等等。
vNode 類型有:註釋節點、文本節點、克隆節點、元素節點、組件節點。
如下是註釋節點、文本節點和克隆節點的代碼:
/* 註釋節點 有效屬性:{isComment: true, text: '註釋節點'} */ export const createEmptyVNode = (text: string = '') => { const node = new VNode() node.text = text // 註釋 node.isComment = true return node } /* 文本節點 有效屬性:{text: '文本節點'} */ export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) } // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // 用於靜態節點和插槽節點 // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. // 克隆節點 export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode( vnode.tag, vnode.data, // #7975 // clone children array to avoid mutating original in case of cloning // a child. vnode.children && vnode.children.slice(), 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 }
克隆節點其實就是將現有節點的全部屬性賦值到新節點中,最後用 cloned.isCloned = true
標記自身是克隆節點。
元素節點一般有如下 4 個屬性:
組件節點與元素節點相似,包含兩個獨有的屬性:
前面已經介紹了虛擬 dom 在 vue 中作的第一件事:提供與真實節點(Node)對應的虛擬節點(vNode);接下來介紹第二件事:將新的虛擬節點與舊的虛擬節點進行對比,找出須要差別,而後更新視圖。
第二件事在 vue 中的實現叫作 patch,即打補丁、修補的意思。經過對比新舊 vNode,找出差別,而後在現有 dom 的基礎上進行修補,從而實現視圖更新。
對比 vNode 找差別是手段,更新視圖纔是目的。
而更新視圖無非就是新增節點、刪除節點和更新節點。接下來咱們逐一分析何時新增節點、在哪裏新增;何時刪除節點,刪除哪一個;何時更新節點,更新哪一個;
注:當 vNode 與 oldVNode 不相同的時候,以 vNode 爲準。
一種狀況是:vNode 存在而 oldVNode 不存在時,須要新增節點。最典型的是初次渲染,由於 odlVNode 是不存在的。
另外一種狀況是 vNode 與 oldVNode 徹底不是同一個節點。這時就須要使用 vNode 生成真實的 dom 節點並插入到 oldVNode 指向的真實 dom 節點旁邊。oldVNode 則是一個被廢棄的節點。例以下面這種狀況:
<div> <p v-if="type === 'A'"> 我是節點A </p> <span v-else-if="type === 'B'"> 我是與A徹底不一樣的節點B </span> </div>
當 type 由 A 變爲 B,節點就會從 p 變成 span,因爲 vNode 與 oldVNode 徹底不是同一個節點,因此須要新增節點。
當節點只在 oldVNode 中存在時,直接將其刪除便可。
前面介紹了新增節點和刪除節點的場景,發現它們有一個共同點:vNode 與 oldVNode 徹底不相同。
但更常見的場景是 vNode 與 oldVNode 是同一個節點。而後咱們須要對它們(vNode 與 oldVNode)進行一個更細緻的對比,再對 oldVNode 對應的真實節點進行更新。
對於文本節點,邏輯天然簡單。首先對比新舊 vNode,發現是同一個節點,而後將 oldVNode 對應的 dom 節點的文本改爲 vNode 中的文本便可。但對於複雜的 vNode,好比界面中的一顆樹組件,這個過程就會變得複雜。
思考一下:前面說到 vNode 的類型有:註釋節點、文本節點、克隆節點、元素節點、組件節點。請問這幾種類型都會被建立並插入到 dom 中嗎?
答:只有註釋節點、文本節點、元素節點。由於 html 只認識這幾種。
因爲只有上面三種節點類型,根據類型作響應的建立,而後插入對應的位置便可。
以元素節點爲例,若是 vNode 有 tag 屬性,則說明是元素節點。則調用 createElement 方法建立對應的節點,接下來就經過 appendChild 方法插入到指定父節點中。若是父元素已經在視圖中,那麼把元素插入到它下面將會自動渲染出來;若是 vNode 的 isComment 屬性是 true,則表示註釋節點;都不是則是文本節點;
一般元素裏面會有子節點,因此這裏涉及一個遞歸的過程,也就是將 vNode 中的 children 依次遍歷,建立節點,而後插入到父節點(父節點也就是剛剛建立出的 dom 節點)中,一層一層的遞歸進行。
請看源碼:
// 建立元素 function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode); } vnode.isRootInsert = !nested; // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } var data = vnode.data; var children = vnode.children; var tag = vnode.tag; // 有 tag 屬性,表示是元素節點 if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) // 建立元素。nodeOps 涉及到跨平臺 : nodeOps.createElement(tag, vnode); setScope(vnode); /* istanbul ignore if */ { // 遞歸建立子節點,並將子節點插入到父節點上 createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } // 將 vnode 對應的元素插入到父元素中 insert(parentElm, vnode.elm, refElm); } // isComment 屬性表示註釋節點 } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); // 插入父節點 insert(parentElm, vnode.elm, refElm); // 不然就是子節點 } else { vnode.elm = nodeOps.createTextNode(vnode.text); // 插入父節點 insert(parentElm, vnode.elm, refElm); } } // 遞歸建立子節點,並將子節點插入到父節點上。vnode 表示父節點 function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children); } // 依次建立子節點,並將子節點插入到父節點中 for (var i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i); } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))); } }
刪除節點很是簡單。直接看源碼:
// 刪除一組指定節點 function removeVnodes (parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { var ch = vnodes[startIdx]; if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch); invokeDestroyHook(ch); } else { // Text node // 刪除個節點 removeNode(ch.elm); } } } } // 刪除單個節點 function removeNode (el) { var parent = nodeOps.parentNode(el); // element may have already been removed due to v-html / v-text if (isDef(parent)) { // nodeOps裏封裝了跨平臺的方法 nodeOps.removeChild(parent, el); } }
有些複雜,並且涉及子節點更新,本文就不展開。
其餘章節請看: