初六和家人出去玩,沒寫完博客。跳票了~
所謂虛擬DOM,是一個用於表示真實 DOM 結構和屬性的 JavaScript 對象,這個對象用於對比虛擬 DOM 和當前真實 DOM 的差別化,而後進行局部渲染從而實現性能上的優化。在Vue.js 中虛擬 DOM 的 JavaScript 對象就是 VNode。
接下來咱們一步步分析:html
既然是虛擬 DOM 的做用是轉爲真實的 DOM,那這就是一個渲染的過程。因此咱們看看 render 方法。在以前的學習中咱們知道了,vue 的渲染函數 _render
方法返回的就是一個 VNode 對象。而在 initRender
初始化渲染的方法中定義的 vm._c
和 vm.$createElement
方法中,createElement
最終也是返回 VNode 對象。因此 VNode 是渲染的關鍵所在。
話很少說,來看看這個VNode爲什麼方神聖。前端
// src/core/vdom/vnode.js export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; // functioanl scope id support 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類型) this.children = children // 當前節點子節點 this.text = text // 當前節點文本 this.elm = elm // 當前節點對應的真實DOM節點 this.ns = undefined // 當前節點命名空間 this.context = context // 當前節點上下文 this.fnContext = undefined // 函數化組件上下文 this.fnOptions = undefined // 函數化組件配置項 this.fnScopeId = undefined // 函數化組件ScopeId this.key = data && data.key // 子節點key屬性 this.componentOptions = componentOptions // 組件配置項 this.componentInstance = undefined // 組件實例 this.parent = undefined // 當前節點父節點 this.raw = false // 是否爲原生HTML或只是普通文本 this.isStatic = false // 靜態節點標誌 keep-alive this.isRootInsert = true // 是否做爲根節點插入 this.isComment = false // 是否爲註釋節點 this.isCloned = false // 是否爲克隆節點 this.isOnce = false // 是否爲v-once節點 this.asyncFactory = asyncFactory // 異步工廠方法 this.asyncMeta = undefined // 異步Meta this.isAsyncPlaceholder = false // 是否爲異步佔位 } // 容器實例向後兼容的別名 get child (): Component | void { return this.componentInstance } }
其實就是一個普通的 JavaScript Class 類,中間有各類數據用於描述虛擬 DOM,下面用一個例子來看看VNode 是如何表現 DOM 的。vue
<div id="app"> <span>{{ message }}</span> <ul> <li v-for="item of list" class="item-cls">{{ item }}</li> </ul> </div> <script> new Vue({ el: '#app', data: { message: 'hello Vue.js', list: ['jack', 'rose', 'james'] } }) </script>
這個例子最終結果如圖:
簡化後的VNode對象結果如圖:node
{ "tag": "div", "data": { "attr": { "id": "app" } }, "children": [ { "tag": "span", "children": [ { "text": "hello Vue.js" } ] }, { "tag": "ul", "children": [ { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "jack" } ] }, { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "rose" } ] }, { "tag": "li", "data": { "staticClass": "item-cls" }, "children": [ { "text": "james" } ] } ] } ], "context": "$Vue$3", "elm": "div#app" }
在看VNode的時候小結如下幾點:git
context
選項都指向了 Vue 實例。elm
屬性則指向了其相對應的真實 DOM 節點。text
沒有 tag
的節點。data
中咱們瞭解了VNode 是如何描述 DOM 以後,來學習如何將虛擬
DOM 變爲真實的 DOM。github
從以前的文章中能夠知道,Vue的渲染過程(不管是初始化視圖仍是更新視圖)最終都將走到 _update
方法中,再來看看這個 _update
方法。web
// src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this if (vm._isMounted) { callHook(vm, 'beforeUpdate') } const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm vm._vnode = vnode if (!prevVnode) { // 初始化渲染 vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) // no need for the ref nodes after initial patch // this prevents keeping a detached DOM tree in memory (#5851) vm.$options._parentElm = vm.$options._refElm = null } else { // 更新渲染 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
不難發現更新試圖都是使用了 vm.__patch__
方法,咱們繼續往下跟。json
// src/platforms/web/runtime/index.js Vue.prototype.__patch__ = inBrowser ? patch : noop
這裏囉嗦一句,要找vue的全局方法,如 vm.aaa
,直接查找 Vue.prototype.aaa
便可。
繼續找下去:數組
// src/platforms/web/runtime/patch.js export const patch: Function = createPatchFunction({ nodeOps, modules })
找到 createPatchFunction
方法~性能優化
// src/core/vdom/patch.js export function createPatchFunction (backend) { …… return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 當前 VNode 未定義、老的 VNode 定義了,調用銷燬鉤子。 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 老的 VNode 未定義,初始化。 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { // 當前 VNode 和老 VNode 都定義了,執行更新操做 // DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp const isRealElement = isDef(oldVnode.nodeType) // 是否爲真實 DOM 元素 if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // 修改已有根節點 patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { // 已有真實 DOM 元素,處理 oldVnode if (isRealElement) { // 掛載一個真實元素,確認是否爲服務器渲染環境或者是否能夠執行成功的合併到真實 DOM 中 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { // 調用 insert 鉤子 // inserted:被綁定元素插入父節點時調用 invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } // 不是服務器渲染或者合併到真實 DOM 失敗,建立一個空節點替換原有節點 oldVnode = emptyNodeAt(oldVnode) } // 替換已有元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 建立新節點 createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 遞歸更新父級佔位節點元素, if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } const insert = ancestor.data.hook.insert if (insert.merged) { for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // 銷燬舊節點 if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } // 調用 insert 鉤子 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
具體解析看代碼註釋~拋開調用生命週期鉤子和銷燬就節點不談,咱們發現代碼中的關鍵在於 createElm
和 patchVnode
方法。
先看 createElm
方法,這個方法建立了真實 DOM 元素。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check // 建立組件 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } 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) } }
重點關注代碼中的方法執行。代碼太多,就不貼出來了,簡單說說用途。
cloneVNode
用於克隆當前 vnode 對象。createComponent
用於建立組件,在調用了組件初始化鉤子以後,初始化組件,而且從新激活組件。在從新激活組件中使用 insert
方法操做 DOM。nodeOps.createElementNS
和 nodeOps.createElement
方法,實際上是真實 DOM 的方法。setScope
用於爲 scoped CSS 設置做用域 ID 屬性createChildren
用於建立子節點,若是子節點是數組,則遍歷執行 createElm
方法,若是子節點的 text 屬性有數據,則使用 nodeOps.appendChild(...)
在真實 DOM 中插入文本內容。insert
用於將元素插入真實 DOM 中。因此,這裏的 nodeOps
指的確定就是真實的 DOM 節點了。最終,這些全部的方法都調用了 nodeOps
中的方法來操做 DOM 元素。
這裏順便科普下 DOM 的屬性和方法。下面把源碼中用到的幾個方法列出來便於學習:
- appendChild: 向元素添加新的子節點,做爲最後一個子節點。
- insertBefore: 在指定的已有的子節點以前插入新節點。
- tagName: 返回元素的標籤名。
- removeChild: 從元素中移除子節點。
- createElementNS: 建立帶有指定命名空間的元素節點。
- createElement: 建立元素節點。
- createComment: 建立註釋節點。
- createTextNode: 建立文本節點。
- setAttribute: 把指定屬性設置或更改成指定值。
- nextSibling: 返回位於相同節點樹層級的下一個節點。
- parentNode: 返回元素父節點。
- setTextContent: 獲取文本內容(這個未在w3school中找到,不過應該就是這個意思了)。
OK,知道以上方法就比較好理解了,createElm
方法的最終目的就是建立真實的 DOM 對象。
看過了建立真實 DOM 後,咱們來學習虛擬 DOM 如何實現 DOM 的更新。這纔是虛擬 DOM 的存在乎義 —— 比對並局部更新 DOM 以達到性能優化的目的。
看代碼~
// 補丁 vnode function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 新舊 vnode 相等 if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm // 異步佔位 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 若是新舊 vnode 爲靜態;新舊 vnode key相同; // 新 vnode 是克隆所得;新 vnode 有 v-once 的屬性 // 則新 vnode 的 componentInstance 用老的 vnode 的。 // 即 vnode 的 componentInstance 保持不變。 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data // 執行 data.hook.prepatch 鉤子。 if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { // 遍歷 cbs,執行 update 方法 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) // 執行 data.hook.update 鉤子 if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 舊 vnode 的 text 選項爲 undefined if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 新舊 vnode 都有 children,且不一樣,執行 updateChildren 方法。 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 清空文本,添加 vnode if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 移除 vnode removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 若是新舊 vnode 都是 undefined,清空文本 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 有不一樣文本內容,更新文本內容 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { // 執行 data.hook.postpatch 鉤子,代表 patch 完畢 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
源碼中添加了一些註釋便於理解,來理一下邏輯。
其中,addVnodes
方法和 removeVnodes
都比較簡單,很好理解。這裏咱們來看看關鍵代碼 updateChildren
方法。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly 是一個只用於 <transition-group> 的特殊標籤, // 確保移除元素過程當中保持一個正確的相對位置。 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // 開始老 vnode 向右一位 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { // 結束老 vnode 向左一位 oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 新舊開始 vnode 類似,進行pacth。開始 vnode 向右一位 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 新舊結束 vnode 類似,進行patch。結束 vnode 向左一位 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 新結束 vnode 和老開始 vnode 類似,進行patch。 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 老開始 vnode 插入到真實 DOM 中,老開始 vnode 向右一位,新結束 vnode 向左一位 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 老結束 vnode 和新開始 vnode 類似,進行 patch。 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 老結束 vnode 插入到真實 DOM 中,老結束 vnode 向左一位,新開始 vnode 向右一位 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 獲取老 Idx 的 key if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 給老 idx 賦值 idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // 若是老 idx 爲 undefined,說明沒有這個元素,建立新 DOM 元素。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { // 獲取 vnode vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { // 若是生成的 vnode 和新開始 vnode 類似,執行 patch。 patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 賦值 undefined,插入 vnodeToMove 元素 oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 相同的key不一樣的元素,視爲新元素 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } // 新開始 vnode 向右一位 newStartVnode = newCh[++newStartIdx] } } // 若是老開始 idx 大於老結束 idx,若是是有效數據則添加 vnode 到新 vnode 中。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 移除 vnode removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
表示已看暈……讓咱們慢慢捋一捋……
嗯……就是這樣!
畢竟是Vue的核心功能之一,雖然省略了很多代碼,但博客篇幅很長。寫了兩天才寫完。不過寫完博客後感受對於 Vue 的理解又加深了不少。
在下一篇博客中,咱們一塊兒來學習template的解析。
鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。
本文源碼已收入到GitHub中,以供參考,固然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos
VioletJack,高效學習前端工程師,喜歡研究提升效率的方法,也專一於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
簡書: http://www.jianshu.com/users/...
Github: https://github.com/violetjack