Vue版本: 2.3.2vue
virtual-dom(後文簡稱vdom)的概念大規模的推廣仍是得益於react出現,virtual-dom也是react這個框架的很是重要的特性之一。相比於頻繁的手動去操做dom而帶來性能問題,vdom很好的將dom作了一層映射關係,進而將在咱們本須要直接進行dom的一系列操做,映射到了操做vdom,而vdom上定義了關於真實dom的一些關鍵的信息,vdom徹底是用js去實現,和宿主瀏覽器沒有任何聯繫,此外得益於js的執行速度,將本來須要在真實dom進行的建立節點,刪除節點,添加節點等一系列複雜的dom操做所有放到vdom中進行,這樣就經過操做vdom來提升直接操做的dom的效率和性能。node
Vue在2.0版本也引入了vdom。其vdom算法是基於snabbdom算法所作的修改。react
在Vue的整個應用生命週期當中,每次須要更新視圖的時候便會使用vdom。那麼在Vue當中,vdom是如何和Vue這個框架融合在一塊兒工做的呢?以及你們經常提到的vdom的diff算法又是怎樣的呢?接下來就經過這篇文章簡單的向你們介紹下Vue當中的vdom是如何去工做的。git
首先,咱們仍是來看下Vue生命週期當中初始化的最後階段:將vm實例掛載到dom上,源碼在src/core/instance/init.jsgithub
Vue.prototype._init = function () {
...
vm.$mount(vm.$options.el)
...
}
複製代碼
其實是調用了src/core/instance/lifecycle.js中的mountComponent方法, mountComponent函數的定義是:算法
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// vm.$el爲真實的node
vm.$el = el
// 若是vm上沒有掛載render函數
if (!vm.$options.render) {
// 空節點
vm.$options.render = createEmptyVNode
}
// 鉤子函數
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
// updateComponent爲監聽函數, new Watcher(vm, updateComponent, noop)
updateComponent = () => {
// Vue.prototype._render 渲染函數
// vm._render() 返回一個VNode
// 更新dom
// vm._render()調用render函數,會返回一個VNode,在生成VNode的過程當中,會動態計算getter,同時推入到dep裏面
vm._update(vm._render(), hydrating)
}
}
// 新建一個_watcher對象
// vm實例上掛載的_watcher主要是爲了更新DOM
// vm/expression/cb
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
複製代碼
注意上面的代碼中定義了一個updateComponent函數,這個函數執行的時候內部會調用vm._update(vm._render(), hyddrating)方法,其中vm._render方法會返回一個新的vnode,(關於vm_render是如何生成vnode的建議你們看看vue的關於compile階段的代碼),而後傳入vm._update方法後,就用這個新的vnode和老的vnode進行diff,最後完成dom的更新工做。那麼updateComponent都是在何時去進行調用呢?express
vm._watcher = new Watcher(vm, updateComponent, noop)
複製代碼
實例化一個watcher,在求值的過程當中this.value = this.lazy ? undefined : this.get(),會調用this.get()方法,所以在實例化的過程中Dep.target會被設爲這個watcher,經過調用vm._render()方法生成新的Vnode並進行diff的過程當中完成了模板當中變量依賴收集工做。即這個watcher被添加到了在模板當中所綁定變量的依賴當中。一旦model中的響應式的數據發生了變化,這些響應式的數據所維護的dep數組便會調用dep.notify()方法完成全部依賴遍歷執行的工做,這裏面就包括了視圖的更新即updateComponent方法,它是在mountComponent中的定義的。數組
updateComponent方法的定義是:瀏覽器
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
複製代碼
完成視圖的更新工做事實上就是調用了vm._update方法,這個方法接收的第一個參數是剛生成的Vnode,調用的vm._update方法(src/core/instance/lifecycle.js)的定義是bash
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
// 新的vnode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 若是須要diff的prevVnode不存在,那麼就用新的vnode建立一個真實dom節點
if (!prevVnode) {
// initial render
// 第一個參數爲真實的node節點
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
// 若是須要diff的prevVnode存在,那麼首先對prevVnode和vnode進行diff,並將須要的更新的dom操做已patch的形式打到prevVnode上,並完成真實dom的更新工做
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
}
}
複製代碼
在這個方法當中最爲關鍵的就是vm.__patch__方法,這也是整個virtaul-dom當中最爲核心的方法,主要完成了prevVnode和vnode的diff過程並根據須要操做的vdom節點打patch,最後生成新的真實dom節點並完成視圖的更新工做。
接下來就讓咱們看下vm.__patch__裏面到底發生了什麼:
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
// 當oldVnode不存在時
if (isUndef(oldVnode)) {
// 建立新的節點
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 對oldVnode和vnode進行diff,並對oldVnode打patch
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}
}
}
在對oldVnode和vnode類型判斷中有個sameVnode方法,這個方法決定了是否須要對oldVnode和vnode進行diff及patch的過程。
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
複製代碼
sameVnode會對傳入的2個vnode進行基本屬性的比較,只有當基本屬性相同的狀況下才認爲這個2個vnode只是局部發生了更新,而後纔會對這2個vnode進行diff,若是2個vnode的基本屬性存在不一致的狀況,那麼就會直接跳過diff的過程,進而依據vnode新建一個真實的dom,同時刪除老的dom節點。
vnode基本屬性的定義能夠參見源碼:src/vdom/vnode.js裏面對於vnode的定義。
constructor (
tag?: string,
data?: VNodeData, // 關於這個節點的data值,包括attrs,style,hook等
children?: ?Array<VNode>, // 子vdom節點
text?: string, // 文本內容
elm?: Node, // 真實的dom節點
context?: Component, // 建立這個vdom的上下文
componentOptions?: VNodeComponentOptions
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = 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
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
複製代碼
每個vnode都映射到一個真實的dom節點上。其中幾個比較重要的屬性:
{
tag: 'div'
data: {
id: 'app',
class: 'page-box'
},
children: [
{
tag: 'p',
text: 'this is demo'
}
]
}
複製代碼
最後渲染出的實際的dom結構就是:
<div id="app" class="page-box">
<p>this is demo</p>
</div>
複製代碼
讓咱們再回到patch函數當中,在當oldVnode不存在的時候,這個時候是root節點初始化的過程,所以調用了createElm(vnode, insertedVnodeQueue, parentElm, refElm)方法去建立一個新的節點。而當oldVnode是vnode且sameVnode(oldVnode, vnode)2個節點的基本屬性相同,那麼就進入了2個節點的diff過程。
diff的過程主要是經過調用patchVnode(src/core/vdom/patch.js)方法進行的:
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
...
}
複製代碼
if (isDef(data) && isPatchable(vnode)) {
// cbs保存了hooks鉤子函數: 'create', 'activate', 'update', 'remove', 'destroy'
// 取出cbs保存的update鉤子函數,依次調用,更新attrs/style/class/events/directives/refs等屬性
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
複製代碼
更新真實dom節點的data屬性,至關於對dom節點進行了預處理的操做
接下來:
...
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 若是vnode沒有文本節點
if (isUndef(vnode.text)) {
// 若是oldVnode的children屬性存在且vnode的屬性也存在
if (isDef(oldCh) && isDef(ch)) {
// updateChildren,對子節點進行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 若是oldVnode的text存在,那麼首先清空text的內容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 而後將vnode的children添加進去
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 刪除elm下的oldchildren
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有子節點,而vnode沒有,那麼就清空這個節點
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 若是oldVnode和vnode文本屬性不一樣,那麼直接更新真是dom節點的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
複製代碼
這其中的diff過程當中又分了好幾種狀況,oldCh爲oldVnode的子節點,ch爲Vnode的子節點:
1.首先進行文本節點的判斷,若oldVnode.text !== vnode.text,那麼就會直接進行文本節點的替換;
2.在vnode沒有文本節點的狀況下,進入子節點的diff;
3.當oldCh和ch都存在且不相同的狀況下,調用updateChildren對子節點進行diff;
4.若oldCh不存在,ch存在,首先清空oldVnode的文本節點,同時調用addVnodes方法將ch添加到elm真實dom節點當中;
5.若oldCh存在,ch不存在,則刪除elm真實節點下的oldCh子節點;
6.若oldVnode有文本節點,而vnode沒有,那麼就清空這個文本節點。
這裏着重分析下updateChildren(src/core/vdom/patch.js)方法,它也是整個diff過程當中最重要的環節
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 爲oldCh和newCh分別創建索引,爲以後遍歷的依據
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, elmToMove, refElm
// 直到oldCh或者newCh被遍歷完後跳出循環
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 插入到老的開始節點的前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 若是以上條件都不知足,那麼這個時候開始比較key值,首先創建key和index索引的對應關係
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
// 若是idxInOld不存在
// 1. newStartVnode上存在這個key,可是oldKeyToIdx中不存在
// 2. newStartVnode上並無設置key屬性
if (isUndef(idxInOld)) { // New element
// 建立新的dom節點
// 插入到oldStartVnode.elm前面
// 參見createElm方法
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
// 將找到的key一致的oldVnode再和newStartVnode進行diff
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
// 移動node節點
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
// 建立新的dom節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
// 若是最後遍歷的oldStartIdx大於oldEndIdx的話
if (oldStartIdx > oldEndIdx) { // 若是是老的vdom先被遍歷完
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// 添加newVnode中剩餘的節點到parentElm中
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) { // 若是是新的vdom先被遍歷完,則刪除oldVnode裏面全部的節點
// 刪除剩餘的節點
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
在開始遍歷diff前,首先給oldCh和newCh分別分配一個startIndex和endIndex來做爲遍歷的索引,當oldCh或者newCh遍歷完後(遍歷完的條件就是oldCh或者newCh的startIndex >= endIndex),就中止oldCh和newCh的diff過程。接下來經過實例來看下整個diff的過程(節點屬性中不帶key的狀況):
1.首先從第一個節點開始比較,不論是oldCh仍是newCh的起始或者終止節點都不存在sameVnode,同時節點屬性中是不帶key標記的,所以第一輪的diff完後,newCh的startVnode被添加到oldStartVnode的前面,同時newStartIndex前移一位;
2.第二輪的diff中,知足sameVnode(oldStartVnode, newStartVnode),所以對這2個vnode進行diff,最後將patch打到oldStartVnode上,同時oldStartVnode和newStartIndex都向前移動一位 3.第三輪的diff中,知足sameVnode(oldEndVnode, newStartVnode),那麼首先對oldEndVnode和newStartVnode進行diff,並對oldEndVnode進行patch,並完成oldEndVnode移位的操做,最後newStartIndex前移一位,oldStartVnode後移一位 4.第四輪的diff中,過程同步驟3; 5.第五輪的diff中,同過程1; 6.遍歷的過程結束後,newStartIdx > newEndIdx,說明此時oldCh存在多餘的節點,那麼最後就須要將這些多餘的節點刪除 在vnode不帶key的狀況下,每一輪的diff過程中都是起始和結束節點進行比較,直到oldCh或者newCh被遍歷完。而當爲vnode引入key屬性後,在每一輪的diff過程當中,當起始和結束節點都沒有找到sameVnode時,首先對oldCh中進行key值與索引的映射:if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
複製代碼
createKeyToOldIdx(src/core/vdom/patch.js)方法,用以將oldCh中的key屬性做爲鍵,而對應的節點的索引做爲值。而後再判斷在newStartVnode的屬性中是否有key,且是否在oldKeyToIndx中找到對應的節點。
1.若是不存在這個key,那麼就將這個newStartVnode做爲新的節點建立且插入到原有的root的子節點中:
if (isUndef(idxInOld)) { // New element
// 建立新的dom節點
// 插入到oldStartVnode.elm前面
// 參見createElm方法
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
複製代碼
2.若是存在這個key,那麼就取出oldCh中的存在這個key的vnode,而後再進行diff的過程:
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
// 將找到的key一致的oldVnode再和newStartVnode進行diff
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
// 清空這個節點
oldCh[idxInOld] = undefined
// 移動node節點
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
// 建立新的dom節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
複製代碼
經過以上分析,給vdom上添加key屬性後,遍歷diff的過程當中,當起始點, 結束點的搜尋及diff出現仍是沒法匹配的狀況下時,就會用key來做爲惟一標識,來進行diff,這樣就能夠提升diff效率。
帶有Key屬性的vnode的diff過程可見下圖:
注意在第一輪的diff事後oldCh上的B節點被刪除了,可是newCh上的B節點上elm屬性保持對oldCh上B節點的elm引用。
原文連接: github.com/DDFE/DDFE-b…