這篇文章將為大家詳細(xì)講解有關(guān)Vue如何實(shí)現(xiàn)virtual-dom的原理,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
成都創(chuàng)新互聯(lián)公司是一家網(wǎng)站設(shè)計(jì)公司,集創(chuàng)意、互聯(lián)網(wǎng)應(yīng)用、軟件技術(shù)為一體的創(chuàng)意網(wǎng)站建設(shè)服務(wù)商,主營(yíng)產(chǎn)品:響應(yīng)式網(wǎng)站設(shè)計(jì)、品牌網(wǎng)站制作、全網(wǎng)營(yíng)銷推廣。我們專注企業(yè)品牌在網(wǎng)站中的整體樹(shù)立,網(wǎng)絡(luò)互動(dòng)的體驗(yàn),以及在手機(jī)等移動(dòng)端的優(yōu)質(zhì)呈現(xiàn)。成都做網(wǎng)站、網(wǎng)站建設(shè)、移動(dòng)互聯(lián)產(chǎn)品、網(wǎng)絡(luò)運(yùn)營(yíng)、VI設(shè)計(jì)、云產(chǎn)品.運(yùn)維為核心業(yè)務(wù)。為用戶提供一站式解決方案,我們深知市場(chǎng)的競(jìng)爭(zhēng)激烈,認(rèn)真對(duì)待每位客戶,為客戶提供賞析悅目的作品,網(wǎng)站的價(jià)值服務(wù)。
Vue是一套用于構(gòu)建用戶界面的漸進(jìn)式JavaScript框架,Vue與其它大型框架的區(qū)別是,使用Vue可以自底向上逐層應(yīng)用,其核心庫(kù)只關(guān)注視圖層,方便與第三方庫(kù)和項(xiàng)目整合,且使用Vue可以采用單文件組件和Vue生態(tài)系統(tǒng)支持的庫(kù)開(kāi)發(fā)復(fù)雜的單頁(yè)應(yīng)用。
virtual-dom(后文簡(jiǎn)稱vdom)的概念大規(guī)模的推廣還是得益于react出現(xiàn),virtual-dom也是react這個(gè)框架的非常重要的特性之一。相比于頻繁的手動(dòng)去操作dom而帶來(lái)性能問(wèn)題,vdom很好的將dom做了一層映射關(guān)系,進(jìn)而將在我們本需要直接進(jìn)行dom的一系列操作,映射到了操作vdom,而vdom上定義了關(guān)于真實(shí)dom的一些關(guān)鍵的信息,vdom完全是用js去實(shí)現(xiàn),和宿主瀏覽器沒(méi)有任何聯(lián)系,此外得益于js的執(zhí)行速度,將原本需要在真實(shí)dom進(jìn)行的創(chuàng)建節(jié)點(diǎn),刪除節(jié)點(diǎn),添加節(jié)點(diǎn)等一系列復(fù)雜的dom操作全部放到vdom中進(jìn)行,這樣就通過(guò)操作vdom來(lái)提高直接操作的dom的效率和性能。
Vue在2.0版本也引入了vdom。其vdom算法是基于snabbdom算法所做的修改。
在Vue的整個(gè)應(yīng)用生命周期當(dāng)中,每次需要更新視圖的時(shí)候便會(huì)使用vdom。那么在Vue當(dāng)中,vdom是如何和Vue這個(gè)框架融合在一起工作的呢?以及大家常常提到的vdom的diff算法又是怎樣的呢?接下來(lái)就通過(guò)這篇文章簡(jiǎn)單的向大家介紹下Vue當(dāng)中的vdom是如何去工作的。
首先,我們還是來(lái)看下Vue生命周期當(dāng)中初始化的最后階段:將vm實(shí)例掛載到dom上,源碼在src/core/instance
Vue.prototype._init = function () { ... vm.$mount(vm.$options.el) // 實(shí)際上是調(diào)用了mountComponent方法 ... }
mountComponent函數(shù)的定義是:
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // vm.$el為真實(shí)的node vm.$el = el // 如果vm上沒(méi)有掛載render函數(shù) if (!vm.$options.render) { // 空節(jié)點(diǎn) vm.$options.render = createEmptyVNode } // 鉤子函數(shù) callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { ... } else { // updateComponent為監(jiān)聽(tīng)函數(shù), new Watcher(vm, updateComponent, noop) updateComponent = () => { // Vue.prototype._render 渲染函數(shù) // vm._render() 返回一個(gè)VNode // 更新dom // vm._render()調(diào)用render函數(shù),會(huì)返回一個(gè)VNode,在生成VNode的過(guò)程中,會(huì)動(dòng)態(tài)計(jì)算getter,同時(shí)推入到dep里面 vm._update(vm._render(), hydrating) } } // 新建一個(gè)_watcher對(duì)象 // vm實(shí)例上掛載的_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 }
注意上面的代碼中定義了一個(gè)updateComponent函數(shù),這個(gè)函數(shù)執(zhí)行的時(shí)候內(nèi)部會(huì)調(diào)用vm._update(vm._render(), hyddrating)方法,其中vm._render方法會(huì)返回一個(gè)新的vnode,(關(guān)于vm_render是如何生成vnode的建議大家看看vue的關(guān)于compile階段的代碼),然后傳入vm._update方法后,就用這個(gè)新的vnode和老的vnode進(jìn)行diff,最后完成dom的更新工作。那么updateComponent都是在什么時(shí)候去進(jìn)行調(diào)用呢?
vm._watcher = new Watcher(vm, updateComponent, noop)
實(shí)例化一個(gè)watcher,在求值的過(guò)程中this.value = this.lazy ? undefined : this.get(),會(huì)調(diào)用this.get()方法,因此在實(shí)例化的過(guò)程當(dāng)中Dep.target會(huì)被設(shè)為這個(gè)watcher,通過(guò)調(diào)用vm._render()方法生成新的Vnode并進(jìn)行diff的過(guò)程中完成了模板當(dāng)中變量依賴收集工作。即這個(gè)watcher被添加到了在模板當(dāng)中所綁定變量的依賴當(dāng)中。一旦model中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化,這些響應(yīng)式的數(shù)據(jù)所維護(hù)的dep數(shù)組便會(huì)調(diào)用dep.notify()方法完成所有依賴遍歷執(zhí)行的工作,這里面就包括了視圖的更新即updateComponent方法的調(diào)用。
updateComponent方法的定義是:
updateComponent = () => { vm._update(vm._render(), hydrating) }
完成視圖的更新工作事實(shí)上就是調(diào)用了vm._update方法,這個(gè)方法接收的第一個(gè)參數(shù)是剛生成的Vnode,調(diào)用的vm._update方法的定義是
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創(chuàng)建一個(gè)真實(shí)dom節(jié)點(diǎn) if (!prevVnode) { // initial render // 第一個(gè)參數(shù)為真實(shí)的node節(jié)點(diǎn) vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) } else { // updates // 如果需要diff的prevVnode存在,那么首先對(duì)prevVnode和vnode進(jìn)行diff,并將需要的更新的dom操作已patch的形式打到prevVnode上,并完成真實(shí)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 } }
在這個(gè)方法當(dāng)中最為關(guān)鍵的就是vm.__patch__方法,這也是整個(gè)virtaul-dom當(dāng)中最為核心的方法,主要完成了prevVnode和vnode的diff過(guò)程并根據(jù)需要操作的vdom節(jié)點(diǎn)打patch,最后生成新的真實(shí)dom節(jié)點(diǎn)并完成視圖的更新工作。
接下來(lái)就讓我們看下vm.__patch__里面到底發(fā)生了什么:
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // 當(dāng)oldVnode不存在時(shí) if (isUndef(oldVnode)) { // 創(chuàng)建新的節(jié)點(diǎn) createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // 對(duì)oldVnode和vnode進(jìn)行diff,并對(duì)oldVnode打patch patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } } }
在對(duì)oldVnode和vnode類型判斷中有個(gè)sameVnode方法,這個(gè)方法決定了是否需要對(duì)oldVnode和vnode進(jìn)行diff及patch的過(guò)程。
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會(huì)對(duì)傳入的2個(gè)vnode進(jìn)行基本屬性的比較,只有當(dāng)基本屬性相同的情況下才認(rèn)為這個(gè)2個(gè)vnode只是局部發(fā)生了更新,然后才會(huì)對(duì)這2個(gè)vnode進(jìn)行diff,如果2個(gè)vnode的基本屬性存在不一致的情況,那么就會(huì)直接跳過(guò)diff的過(guò)程,進(jìn)而依據(jù)vnode新建一個(gè)真實(shí)的dom,同時(shí)刪除老的dom節(jié)點(diǎn)。
vnode基本屬性的定義可以參見(jiàn)源碼:src/vdom/vnode.js里面對(duì)于vnode的定義。
constructor ( tag?: string, data?: VNodeData, // 關(guān)于這個(gè)節(jié)點(diǎn)的data值,包括attrs,style,hook等 children?: ?Array, // 子vdom節(jié)點(diǎn) text?: string, // 文本內(nèi)容 elm?: Node, // 真實(shí)的dom節(jié)點(diǎn) context?: Component, // 創(chuàng)建這個(gè)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 } }
每一個(gè)vnode都映射到一個(gè)真實(shí)的dom節(jié)點(diǎn)上。其中幾個(gè)比較重要的屬性:
tag 屬性即這個(gè)vnode的標(biāo)簽屬性
data 屬性包含了最后渲染成真實(shí)dom節(jié)點(diǎn)后,節(jié)點(diǎn)上的class,attribute,style以及綁定的事件
children 屬性是vnode的子節(jié)點(diǎn)
text 屬性是文本屬性
elm 屬性為這個(gè)vnode對(duì)應(yīng)的真實(shí)dom節(jié)點(diǎn)
key 屬性是vnode的標(biāo)記,在diff過(guò)程中可以提高diff的效率,后文有講解
比如,我定義了一個(gè)vnode,它的數(shù)據(jù)結(jié)構(gòu)是:
{ tag: 'div' data: { id: 'app', class: 'page-box' }, children: [ { tag: 'p', text: 'this is demo' } ] }
最后渲染出的實(shí)際的dom結(jié)構(gòu)就是:
this is demo
讓我們?cè)倩氐絧atch函數(shù)當(dāng)中,在當(dāng)oldVnode不存在的時(shí)候,這個(gè)時(shí)候是root節(jié)點(diǎn)初始化的過(guò)程,因此調(diào)用了createElm(vnode, insertedVnodeQueue, parentElm, refElm)方法去創(chuàng)建一個(gè)新的節(jié)點(diǎn)。而當(dāng)oldVnode是vnode且sameVnode(oldVnode, vnode)2個(gè)節(jié)點(diǎn)的基本屬性相同,那么就進(jìn)入了2個(gè)節(jié)點(diǎn)的diff過(guò)程。
diff的過(guò)程主要是通過(guò)調(diào)用patchVnode方法進(jìn)行的:
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { ... }
if (isDef(data) && isPatchable(vnode)) { // cbs保存了hooks鉤子函數(shù): 'create', 'activate', 'update', 'remove', 'destroy' // 取出cbs保存的update鉤子函數(shù),依次調(diào)用,更新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) }
更新真實(shí)dom節(jié)點(diǎn)的data屬性,相當(dāng)于對(duì)dom節(jié)點(diǎn)進(jìn)行了預(yù)處理的操作
接下來(lái):
... const elm = vnode.elm = oldVnode.elm const oldCh = oldVnode.children const ch = vnode.children // 如果vnode沒(méi)有文本節(jié)點(diǎn) if (isUndef(vnode.text)) { // 如果oldVnode的children屬性存在且vnode的屬性也存在 if (isDef(oldCh) && isDef(ch)) { // updateChildren,對(duì)子節(jié)點(diǎn)進(jìn)行diff if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 如果oldVnode的text存在,那么首先清空text的內(nèi)容 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 然后將vnode的children添加進(jìn)去 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有子節(jié)點(diǎn),而vnode沒(méi)有,那么就清空這個(gè)節(jié)點(diǎn) nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點(diǎn)的文本元素 nodeOps.setTextContent(elm, vnode.text) }
這其中的diff過(guò)程中又分了好幾種情況,oldCh為oldVnode的子節(jié)點(diǎn),ch為Vnode的子節(jié)點(diǎn):
首先進(jìn)行文本節(jié)點(diǎn)的判斷,若oldVnode.text !== vnode.text,那么就會(huì)直接進(jìn)行文本節(jié)點(diǎn)的替換;
在vnode沒(méi)有文本節(jié)點(diǎn)的情況下,進(jìn)入子節(jié)點(diǎn)的diff;
當(dāng)oldCh和ch都存在且不相同的情況下,調(diào)用updateChildren對(duì)子節(jié)點(diǎn)進(jìn)行diff;
若oldCh不存在,ch存在,首先清空oldVnode的文本節(jié)點(diǎn),同時(shí)調(diào)用addVnodes方法將ch添加到elm真實(shí)dom節(jié)點(diǎn)當(dāng)中;
若oldCh存在,ch不存在,則刪除elm真實(shí)節(jié)點(diǎn)下的oldCh子節(jié)點(diǎn);
若oldVnode有文本節(jié)點(diǎn),而vnode沒(méi)有,那么就清空這個(gè)文本節(jié)點(diǎn)。
這里著重分析下updateChildren方法,它也是整個(gè)diff過(guò)程中最重要的環(huán)節(jié):
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 為oldCh和newCh分別建立索引,為之后遍歷的依據(jù) 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被遍歷完后跳出循環(huán) 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) // 插入到老的開(kāi)始節(jié)點(diǎn)的前面 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 如果以上條件都不滿足,那么這個(gè)時(shí)候開(kāi)始比較key值,首先建立key和index索引的對(duì)應(yīng)關(guān)系 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null // 如果idxInOld不存在 // 1. newStartVnode上存在這個(gè)key,但是oldKeyToIdx中不存在 // 2. newStartVnode上并沒(méi)有設(shè)置key屬性 if (isUndef(idxInOld)) { // New element // 創(chuàng)建新的dom節(jié)點(diǎn) // 插入到oldStartVnode.elm前面 // 參見(jiàn)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進(jìn)行diff if (sameVnode(elmToMove, newStartVnode)) { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) oldCh[idxInOld] = undefined // 移動(dòng)node節(jié)點(diǎn) canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element // 創(chuàng)建新的dom節(jié)點(diǎn) 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中剩余的節(jié)點(diǎn)到parentElm中 addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { // 如果是新的vdom先被遍歷完,則刪除oldVnode里面所有的節(jié)點(diǎn) // 刪除剩余的節(jié)點(diǎn) removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
在開(kāi)始遍歷diff前,首先給oldCh和newCh分別分配一個(gè)startIndex和endIndex來(lái)作為遍歷的索引,當(dāng)oldCh或者newCh遍歷完后(遍歷完的條件就是oldCh或者newCh的startIndex >= endIndex),就停止oldCh和newCh的diff過(guò)程。接下來(lái)通過(guò)實(shí)例來(lái)看下整個(gè)diff的過(guò)程(節(jié)點(diǎn)屬性中不帶key的情況):
首先從第一個(gè)節(jié)點(diǎn)開(kāi)始比較,不管是oldCh還是newCh的起始或者終止節(jié)點(diǎn)都不存在sameVnode,同時(shí)節(jié)點(diǎn)屬性中是不帶key標(biāo)記的,因此第一輪的diff完后,newCh的startVnode被添加到oldStartVnode的前面,同時(shí)newStartIndex前移一位;
第二輪的diff中,滿足sameVnode(oldStartVnode, newStartVnode),因此對(duì)這2個(gè)vnode進(jìn)行diff,最后將patch打到oldStartVnode上,同時(shí)oldStartVnode和newStartIndex都向前移動(dòng)一位
第三輪的diff中,滿足sameVnode(oldEndVnode, newStartVnode),那么首先對(duì)oldEndVnode和newStartVnode進(jìn)行diff,并對(duì)oldEndVnode進(jìn)行patch,并完成oldEndVnode移位的操作,最后newStartIndex前移一位,oldStartVnode后移一位;
第四輪的diff中,過(guò)程同步驟3;
第五輪的diff中,同過(guò)程1;
遍歷的過(guò)程結(jié)束后,newStartIdx > newEndIdx,說(shuō)明此時(shí)oldCh存在多余的節(jié)點(diǎn),那么最后就需要將這些多余的節(jié)點(diǎn)刪除。
在vnode不帶key的情況下,每一輪的diff過(guò)程當(dāng)中都是起始和結(jié)束節(jié)點(diǎn)進(jìn)行比較,直到oldCh或者newCh被遍歷完。而當(dāng)為vnode引入key屬性后,在每一輪的diff過(guò)程中,當(dāng)起始和結(jié)束節(jié)點(diǎn)都沒(méi)有找到sameVnode時(shí),首先對(duì)oldCh中進(jìn)行key值與索引的映射:
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
createKeyToOldIdx方法,用以將oldCh中的key屬性作為鍵,而對(duì)應(yīng)的節(jié)點(diǎn)的索引作為值。然后再判斷在newStartVnode的屬性中是否有key,且是否在oldKeyToIndx中找到對(duì)應(yīng)的節(jié)點(diǎn)。
如果不存在這個(gè)key,那么就將這個(gè)newStartVnode作為新的節(jié)點(diǎn)創(chuàng)建且插入到原有的root的子節(jié)點(diǎn)中:
if (isUndef(idxInOld)) { // New element // 創(chuàng)建新的dom節(jié)點(diǎn) // 插入到oldStartVnode.elm前面 // 參見(jiàn)createElm方法 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] }
如果存在這個(gè)key,那么就取出oldCh中的存在這個(gè)key的vnode,然后再進(jìn)行diff的過(guò)程:
elmToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !elmToMove) { // 將找到的key一致的oldVnode再和newStartVnode進(jìn)行diff if (sameVnode(elmToMove, newStartVnode)) { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) // 清空這個(gè)節(jié)點(diǎn) oldCh[idxInOld] = undefined // 移動(dòng)node節(jié)點(diǎn) canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // same key but different element. treat as new element // 創(chuàng)建新的dom節(jié)點(diǎn) createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] }
通過(guò)以上分析,給vdom上添加key屬性后,遍歷diff的過(guò)程中,當(dāng)起始點(diǎn), 結(jié)束點(diǎn)的搜尋及diff出現(xiàn)還是無(wú)法匹配的情況下時(shí),就會(huì)用key來(lái)作為唯一標(biāo)識(shí),來(lái)進(jìn)行diff,這樣就可以提高diff效率。
帶有Key屬性的vnode的diff過(guò)程可見(jiàn)下圖:
注意在第一輪的diff過(guò)后oldCh上的B節(jié)點(diǎn)被刪除了,但是newCh上的B節(jié)點(diǎn)上elm屬性保持對(duì)oldCh上B節(jié)點(diǎn)的elm引用。
關(guān)于“Vue如何實(shí)現(xiàn)virtual-dom的原理”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。