DOM
是文檔對象模型(Document Object Model
)的簡寫,在瀏覽器中咱們能夠經過js來操做DOM
,可是這樣的操做性能不好,因而Virtual Dom
應運而生。個人理解,Virtual Dom
就是在js中模擬DOM
對象樹來優化DOM
操做的一種技術或思路。html
本文將對於Vue框架2.1.8版本中使用的Virtual Dom
進行分析。vue
一個VNode的實例對象包含了如下屬性node
tag
: 當前節點的標籤名web
data
: 當前節點的數據對象,具體包含哪些字段能夠參考vue源碼types/vnode.d.ts
中對VNodeData
的定義
算法
children
: 數組類型,包含了當前節點的子節點數組
text
: 當前節點的文本,通常文本節點或註釋節點會有該屬性瀏覽器
elm
: 當前虛擬節點對應的真實的dom節點app
ns
: 節點的namespace框架
context
: 編譯做用域dom
functionalContext
: 函數化組件的做用域
key
: 節點的key屬性,用於做爲節點的標識,有利於patch的優化
componentOptions
: 建立組件實例時會用到的選項信息
child
: 當前節點對應的組件實例
parent
: 組件的佔位節點
raw
: raw html
isStatic
: 靜態節點的標識
isRootInsert
: 是否做爲根節點插入,被<transition>
包裹的節點,該屬性的值爲false
isComment
: 當前節點是不是註釋節點
isCloned
: 當前節點是否爲克隆節點
isOnce
: 當前節點是否有v-once
指令
VNode
能夠理解爲vue框架的虛擬dom的基類,經過new
實例化的VNode
大體能夠分爲幾類
EmptyVNode
: 沒有內容的註釋節點
TextVNode
: 文本節點
ElementVNode
: 普通元素節點
ComponentVNode
: 組件節點
CloneVNode
: 克隆節點,能夠是以上任意類型的節點,惟一的區別在於isCloned
屬性爲true
...
const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不傳data的狀況 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 若是alwaysNormalize是true // 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 調用_createElement建立虛擬節點 return _createElement(context, tag, data, children, normalizationType) } function _createElement (context, tag, data, children, normalizationType) { /** * 若是存在data.__ob__,說明data是被Observer觀察的數據 * 不能用做虛擬節點的data * 須要拋出警告,並返回一個空節點 * * 被監控的data不能被用做vnode渲染的數據的緣由是: * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做 */ if (data && data.__ob__) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // 當組件的is屬性被設置爲一個falsy的值 // Vue將不會知道要把這個組件渲染成什麼 // 因此渲染一個空節點 if (!tag) { return createEmptyVNode() } // 做用域插槽 if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根據normalizationType的值,選擇不一樣的處理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 若是標籤名是字符串類型 if (typeof tag === 'string') { let Ctor // 獲取標籤名的命名空間 ns = config.getTagNamespace(tag) // 判斷是否爲保留標籤 if (config.isReservedTag(tag)) { // 若是是保留標籤,就建立一個這樣的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義 } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) { // 若是找到了這個標籤的定義,就以此建立虛擬組件節點 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常建立一個vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 當tag不是字符串的時候,咱們認爲tag是組件的構造類 // 因此直接建立 } else { vnode = createComponent(tag, data, context, children) } // 若是有vnode if (vnode) { // 若是有namespace,就應用下namespace,而後返回vnode if (ns) applyNS(vnode, ns) return vnode // 不然,返回一個空節點 } else { return createEmptyVNode() } }
簡單的梳理了一個流程圖,能夠參考下
patch
函數的定義在src/core/vdom/patch.js
中,咱們先來看下這個函數的邏輯
patch
函數接收6個參數:
oldVnode
: 舊的虛擬節點或舊的真實dom節點
vnode
: 新的虛擬節點
hydrating
: 是否要跟真是dom混合
removeOnly
: 特殊flag,用於<transition-group>
組件
parentElm
: 父節點
refElm
: 新節點將插入到refElm
以前
patch
的策略是:
若是vnode
不存在可是oldVnode
存在,說明意圖是要銷燬老節點,那麼就調用invokeDestroyHook(oldVnode)
來進行銷燬
若是oldVnode
不存在可是vnode
存在,說明意圖是要建立新節點,那麼就調用createElm
來建立新節點
當vnode
和oldVnode
都存在時
若是oldVnode
和vnode
是同一個節點,就調用patchVnode
來進行patch
當vnode
和oldVnode
不是同一個節點時,若是oldVnode
是真實dom節點或hydrating
設置爲true
,須要用hydrate
函數將虛擬dom和真是dom進行映射,而後將oldVnode
設置爲對應的虛擬dom,找到oldVnode.elm
的父節點,根據vnode建立一個真實dom節點並插入到該父節點中oldVnode.elm
的位置
這裏面值得一提的是patchVnode
函數,由於真正的patch算法是由它來實現的(patchVnode中更新子節點的算法實際上是在updateChildren
函數中實現的,爲了便於理解,我統一放到patchVnode
中來解釋)。
patchVnode
算法是:
若是oldVnode
跟vnode
徹底一致,那麼不須要作任何事情
若是oldVnode
跟vnode
都是靜態節點,且具備相同的key
,當vnode
是克隆節點或是v-once
指令控制的節點時,只須要把oldVnode.elm
和oldVnode.child
都複製到vnode
上,也不用再有其餘操做
不然,若是vnode
不是文本節點或註釋節點
若是oldVnode
和vnode
都有子節點,且2方的子節點不徹底一致,就執行更新子節點的操做(這一部分實際上是在updateChildren
函數中實現),算法以下
分別獲取oldVnode
和vnode
的firstChild
、lastChild
,賦值給oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
若是oldStartVnode
和newStartVnode
是同一節點,調用patchVnode
進行patch
,而後將oldStartVnode
和newStartVnode
都設置爲下一個子節點,重複上述流程
若是oldEndVnode
和newEndVnode
是同一節點,調用patchVnode
進行patch
,而後將oldEndVnode
和newEndVnode
都設置爲上一個子節點,重複上述流程
若是oldStartVnode
和newEndVnode
是同一節點,調用patchVnode
進行patch
,若是removeOnly
是false
,那麼能夠把oldStartVnode.elm
移動到oldEndVnode.elm
以後,而後把oldStartVnode
設置爲下一個節點,newEndVnode
設置爲上一個節點,重複上述流程
若是newStartVnode
和oldEndVnode
是同一節點,調用patchVnode
進行patch
,若是removeOnly
是false
,那麼能夠把oldEndVnode.elm
移動到oldStartVnode.elm
以前,而後把newStartVnode
設置爲下一個節點,oldEndVnode
設置爲上一個節點,重複上述流程
若是以上都不匹配,就嘗試在oldChildren
中尋找跟newStartVnode
具備相同key
的節點,若是找不到相同key
的節點,說明newStartVnode
是一個新節點,就建立一個,而後把newStartVnode
設置爲下一個節點
若是上一步找到了跟newStartVnode
相同key
的節點,那麼經過其餘屬性的比較來判斷這2個節點是不是同一個節點,若是是,就調用patchVnode
進行patch
,若是removeOnly
是false
,就把newStartVnode.elm
插入到oldStartVnode.elm
以前,把newStartVnode
設置爲下一個節點,重複上述流程
若是在oldChildren
中沒有尋找到newStartVnode
的同一節點,那就建立一個新節點,把newStartVnode
設置爲下一個節點,重複上述流程
若是oldStartVnode
跟oldEndVnode
重合了,而且newStartVnode
跟newEndVnode
也重合了,這個循環就結束了
若是隻有oldVnode
有子節點,那就把這些節點都刪除
若是隻有vnode
有子節點,那就建立這些子節點
若是oldVnode
和vnode
都沒有子節點,可是oldVnode
是文本節點或註釋節點,就把vnode.elm
的文本設置爲空字符串
若是vnode
是文本節點或註釋節點,可是vnode.text != oldVnode.text
時,只須要更新vnode.elm
的文本內容就能夠
patch
提供了5個生命週期鉤子,分別是
create
: 建立patch時
activate
: 激活組件時
update
: 更新節點時
remove
: 移除節點時
destroy
: 銷燬節點時
這些鉤子是提供給Vue內部的directives
/ref
/attrs
/style
等模塊使用的,方便這些模塊在patch的不一樣階段進行相應的操做,這裏模塊定義在src/core/vdom/modules
和src/platforms/web/runtime/modules
2個目錄中
vnode
也提供了生命週期鉤子,分別是
init
: vdom初始化時
create
: vdom建立時
prepatch
: patch以前
insert
: vdom插入後
update
: vdom更新前
postpatch
: patch以後
remove
: vdom移除時
destroy
: vdom銷燬時
vue組件的生命週期底層其實就依賴於vnode的生命週期,在src/core/vdom/create-component.js
中咱們能夠看到,vue爲本身的組件vnode已經寫好了默認的init
/prepatch
/insert
/destroy
,而vue組件的mounted
/activated
就是在insert
中觸發的,deactivated
就是在destroy
中觸發的
在Vue裏面,Vue.prototype.$createElement
對應vdom的createElement
方法,Vue.prototype.__patch__
對應patch
方法,我寫了個簡單的demo來驗證下功能
<p data-height="265" data-theme-id="0" data-slug-hash="rjZKZz" data-default-tab="html,result" data-user="JoeRay" data-embed-version="2" data-pen-title="Vue Virtual Dom" class="codepen">See the Pen Vue Virtual Dom by zhulei (@JoeRay) on CodePen.</p>
<script async src="https://production-assets.cod...