node.js極速入門課程:進(jìn)入學(xué)習(xí)
虛擬dom
和diff
算法是vue
學(xué)習(xí)過程中的一個(gè)難點(diǎn),也是面試中必須掌握的一個(gè)知識(shí)點(diǎn)。這兩者相輔相成,是vue
框架的核心。今天我們再來總結(jié)下vue2
中的虛擬dom
和 diff
算法。(學(xué)習(xí)視頻分享:vue視頻教程)
什么是 VNode
我們知道,render function
會(huì)被轉(zhuǎn)化成 VNode
。VNode
其實(shí)就是一棵以 JavaScript
對象作為基礎(chǔ)的樹,用對象屬性來描述節(jié)點(diǎn),實(shí)際上它只是一層對真實(shí) DOM
的抽象。最終可以通過一系列操作使這棵樹映射到真實(shí)環(huán)境上。
比如有如下template
<template> <span class="demo" v-show="isShow"> This is a span. </span> </template>
它換成 VNode
以后大概就是下面這個(gè)樣子
{ tag: "span", data: { /* 指令集合數(shù)組 */ directives: [ { /* v-show指令 */ rawName: "v-show", expression: "isShow", name: "show", value: true, }, ], /* 靜態(tài)class */ staticClass: "demo", }, text: undefined, children: [ /* 子節(jié)點(diǎn)是一個(gè)文本VNode節(jié)點(diǎn) */ { tag: undefined, data: undefined, text: "This is a span.", children: undefined, }, ], };
總的來說,VNode
就是一個(gè) JavaScript
對象。這個(gè)JavaScript
對象能完整地表示出真實(shí)DOM
。
為什么vue要使用 VNode
筆者認(rèn)為有兩點(diǎn)原因
-
由于
Virtual DOM
是以JavaScript
對象為基礎(chǔ)而不依賴真實(shí)平臺(tái)環(huán)境,所以使它具有了跨平臺(tái)的能力,比如說瀏覽器平臺(tái)、Weex、Node 等。 -
減少操作
DOM
,任何頁面的變化,都只使用VNode
進(jìn)行操作對比,只需要在最后一次進(jìn)行掛載更新DOM
,避免了頻繁操作DOM
,減少了瀏覽器的回流和重繪從而提高頁面性能。
diff算法
下面我們來看看組件更新所涉及到的diff算法
。
前面我們講依賴收集的時(shí)候有說到,渲染watcher
傳遞給Watcher
的get
方法其實(shí)是updateComponent
方法。
updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
所以組件在響應(yīng)式數(shù)據(jù)發(fā)生變化的時(shí)候會(huì)再次觸發(fā)該方法,接下來我們來詳細(xì)分析一下updateComponent
里面的_update
方法。
_update
在_update
方法中做了初始渲染和更新的區(qū)分,雖然都是調(diào)用__patch__
方法,但是傳遞的參數(shù)不一樣。
// src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode vm._vnode = vnode // 初次渲染沒有 prevVnode,組件更新才會(huì)有 if (!prevVnode) { // 初次渲染 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } // ... }
下面我們再來看看__patch__
方法
__patch__
patch
方法接收四個(gè)參數(shù),由于初始渲染的時(shí)候oldVnode
為vm.$el
是null
,所以初始渲染是沒有oldVnode
。
// src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly) { // 新節(jié)點(diǎn)不存在,只有oldVnode就直接銷毀,然后返回 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 沒有老節(jié)點(diǎn),直接創(chuàng)建,也就是初始渲染 if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) // 不是真實(shí)dom,并且是相同節(jié)點(diǎn)走patch if (!isRealElement && sameVnode(oldVnode, vnode)) { // 這里才會(huì)涉及到diff算法 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // ... } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 1.創(chuàng)建一個(gè)新節(jié)點(diǎn) createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 2.更新父節(jié)點(diǎn)占位符 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) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // 3.刪除老節(jié)點(diǎn) if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } //觸發(fā)插入鉤子 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
patch
方法大概流程如下:
-
沒有新節(jié)點(diǎn)只有老節(jié)點(diǎn)直接刪除老節(jié)點(diǎn)。
-
只有新節(jié)點(diǎn)沒有老節(jié)點(diǎn)直接添加新節(jié)點(diǎn)。
-
既有新節(jié)點(diǎn)又有老節(jié)點(diǎn)則判斷是不是相同節(jié)點(diǎn),相同則進(jìn)入
pathVnode
。patchVnode
我們后面會(huì)重點(diǎn)分析。 -
既有新節(jié)點(diǎn)又有老節(jié)點(diǎn)則判斷是不是相同節(jié)點(diǎn),不相同則直接刪除老節(jié)點(diǎn)添加新節(jié)點(diǎn)。
我們再來看看它是怎么判斷是同一個(gè)節(jié)點(diǎn)的。
// src/core/vdom/patch.js function sameVnode (a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
判斷兩個(gè)VNode
節(jié)點(diǎn)是否是同一個(gè)節(jié)點(diǎn),需要同時(shí)滿足以下條件
-
key
相同 -
都有異步組件工廠函數(shù)
-
tag
(當(dāng)前節(jié)點(diǎn)的標(biāo)簽名)相同 -
isComment
是否同為注釋節(jié)點(diǎn) -
是否
data
(當(dāng)前節(jié)點(diǎn)對應(yīng)的對象,包含了具體的一些數(shù)據(jù)信息,是一個(gè)VNodeData類型) -
當(dāng)標(biāo)簽是
<input>
的時(shí)候,type
必須相同
當(dāng)兩個(gè)VNode
的tag、key、isComment
都相同,并且同時(shí)定義或未定義data
的時(shí)候,且如果標(biāo)簽為input則type
必須相同。這時(shí)候這兩個(gè)VNode
則算sameVnode
,可以直接進(jìn)行patchVnode
操作。
patchVnode
下面我們再來詳細(xì)分析下patchVnode
方法。
// src/core/vdom/patch.js function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 兩個(gè)vnode相同則直接返回 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } 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都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)), 并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次), 那么只需要替換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 /*調(diào)用prepatch鉤子*/ if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // 獲取新老虛擬節(jié)點(diǎn)的子節(jié)點(diǎn) const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { 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) } // 新節(jié)點(diǎn)不是文本節(jié)點(diǎn) if (isUndef(vnode.text)) { /*新老節(jié)點(diǎn)均有children子節(jié)點(diǎn),則對子節(jié)點(diǎn)進(jìn)行diff操作,調(diào)用updateChildren*/ if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) /*如果只有新節(jié)點(diǎn)有子節(jié)點(diǎn),先清空elm文本內(nèi)容,然后為當(dāng)前DOM節(jié)點(diǎn)加入子節(jié)點(diǎn)。*/ } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) /*如果只有老節(jié)點(diǎn)有子節(jié)點(diǎn),則移除elm所有子節(jié)點(diǎn)*/ } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) /*當(dāng)新老節(jié)點(diǎn)都無子節(jié)點(diǎn)的時(shí)候,因?yàn)檫@個(gè)邏輯中新節(jié)點(diǎn)text不存在,所以直接去除ele的文本*/ } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 新節(jié)點(diǎn)是文本節(jié)點(diǎn),如果文本不一樣就設(shè)置新的文本 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } /*調(diào)用postpatch鉤子*/ if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
patchVnode
方法大概流程如下:
1.新老節(jié)點(diǎn)相同就直接返回。
2.如果新舊VNode都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)),并且新的VNode是clone或者是標(biāo)記了once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換componentInstance即可。
3.新節(jié)點(diǎn)不是文本節(jié)點(diǎn),新老節(jié)點(diǎn)均有children
子節(jié)點(diǎn),則對子節(jié)點(diǎn)進(jìn)行diff
操作,調(diào)用updateChildren
,這個(gè)updateChildren
是diff算法
的核心,后面我們會(huì)重點(diǎn)說。
4.新節(jié)點(diǎn)不是文本節(jié)點(diǎn),如果老節(jié)點(diǎn)沒有子節(jié)點(diǎn)而新節(jié)點(diǎn)存在子節(jié)點(diǎn),先清空老節(jié)點(diǎn)DOM的文本內(nèi)容,然后為當(dāng)前DOM節(jié)點(diǎn)加入子節(jié)點(diǎn)。
5.新節(jié)點(diǎn)不是文本節(jié)點(diǎn),當(dāng)新節(jié)點(diǎn)沒有子節(jié)點(diǎn)而老節(jié)點(diǎn)有子節(jié)點(diǎn)的時(shí)候,則移除該DOM節(jié)點(diǎn)的所有子節(jié)點(diǎn)。
6.新節(jié)點(diǎn)不是文本節(jié)點(diǎn),并且新老節(jié)點(diǎn)都無子節(jié)點(diǎn)的時(shí)候,只需要將老節(jié)點(diǎn)文本清空。
7.新節(jié)點(diǎn)是文本節(jié)點(diǎn),并且新老節(jié)點(diǎn)文本不一樣,則進(jìn)行文本的替換。
updateChildren(diff算法核心)
updateChildren
是diff
算法的核心,下面我們來重點(diǎn)分析。
這兩張圖代表舊的VNode與新VNode進(jìn)行patch的過程,他們只是在同層級(jí)的VNode之間進(jìn)行比較得到變化(相同顏色的方塊代表互相進(jìn)行比較的VNode節(jié)點(diǎn)),然后修改變化的視圖,所以十分高效。所以Diff算法是:深度優(yōu)先算法
。 時(shí)間復(fù)雜度:O(n)
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 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] // 老 VNode 節(jié)點(diǎn)的頭部與新 VNode 節(jié)點(diǎn)的頭部是相同的 VNode 節(jié)點(diǎn),直接進(jìn)行 patchVnode,同時(shí) oldStartIdx 與 newStartIdx 向后移動(dòng)一位。 } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 兩個(gè) VNode 的結(jié)尾是相同的 VNode,同樣進(jìn)行 patchVnode 操作。并將 oldEndVnode 與 newEndVnode 向前移動(dòng)一位。 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode 與 newEndVnode 符合 sameVnode 的時(shí)候, // 將 oldStartVnode.elm 這個(gè)節(jié)點(diǎn)直接移動(dòng)到 oldEndVnode.elm 這個(gè)節(jié)點(diǎn)的后面即可。 // 然后 oldStartIdx 向后移動(dòng)一位,newEndIdx 向前移動(dòng)一位。 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // oldEndVnode 與 newStartVnode 符合 sameVnode 時(shí), // 將 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。 // oldEndIdx 向前移動(dòng)一位,newStartIdx 向后移動(dòng)一位。 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // createKeyToOldIdx 的作用是產(chǎn)生 key 與 index 索引對應(yīng)的一個(gè) map 表 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果沒有找到相同的節(jié)點(diǎn),則通過 createElm 創(chuàng)建一個(gè)新節(jié)點(diǎn),并將 newStartIdx 向后移動(dòng)一位。 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] // 如果找到了節(jié)點(diǎn),同時(shí)它符合 sameVnode,則將這兩個(gè)節(jié)點(diǎn)進(jìn)行 patchVnode,將該位置的老節(jié)點(diǎn)賦值 undefined // 同時(shí)將 newStartVnode.elm 插入到 oldStartVnode.elm 的前面 if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果不符合 sameVnode,只能創(chuàng)建一個(gè)新節(jié)點(diǎn)插入到 parentElm 的子節(jié)點(diǎn)中,newStartIdx 往后移動(dòng)一位。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // 當(dāng) while 循環(huán)結(jié)束以后,如果 oldStartIdx > oldEndIdx,說明老節(jié)點(diǎn)比對完了,但是新節(jié)點(diǎn)還有多的, // 需要將新節(jié)點(diǎn)插入到真實(shí) DOM 中去,調(diào)用 addVnodes 將這些節(jié)點(diǎn)插入即可。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 如果滿足 newStartIdx > newEndIdx 條件,說明新節(jié)點(diǎn)比對完了,老節(jié)點(diǎn)還有多, // 將這些無用的老節(jié)點(diǎn)通過 removeVnodes 批量刪除即可。 } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
vue2
的diff
算法采用的是雙端比較
,所謂雙端比較
就是新列表和舊列表兩個(gè)列表的頭與尾互相對比,在對比的過程中指針會(huì)逐漸向內(nèi)靠攏,直到某一個(gè)列表的節(jié)點(diǎn)全部遍歷過,對比停止。
首尾對比的四種情況
我們首先來看看首尾對比的四種情況。
-
使用舊列表的頭一個(gè)節(jié)點(diǎn)
oldStartNode
與新列表的頭一個(gè)節(jié)點(diǎn)newStartNode
對比 -
使用舊列表的最后一個(gè)節(jié)點(diǎn)
oldEndNode
與新列表的最后一個(gè)節(jié)點(diǎn)newEndNode
對比 -
使用舊列表的頭一個(gè)節(jié)點(diǎn)
oldStartNode
與新列表的最后一個(gè)節(jié)點(diǎn)newEndNode
對比 -
使用舊列表的最后一個(gè)節(jié)點(diǎn)
oldEndNode
與新列表的頭一個(gè)節(jié)點(diǎn)newStartNode
對比
首先是 oldStartVnode
與 newStartVnode
符合 sameVnode
時(shí),說明老 VNode 節(jié)點(diǎn)的頭部與新 VNode 節(jié)點(diǎn)的頭部是相同的 VNode 節(jié)點(diǎn),直接進(jìn)行 patchVnode
,同時(shí) oldStartIdx
與 newStartIdx
向后移動(dòng)一位。
其次是 oldEndVnode
與 newEndVnode
符合 sameVnode
,也就是兩個(gè) VNode 的結(jié)尾是相同的 VNode,同樣進(jìn)行 patchVnode
操作并將 oldEndVnode
與 newEndVnode
向前移動(dòng)一位。
接下來是兩種交叉的情況。
先是 oldStartVnode
與 newEndVnode
符合 sameVnode
的時(shí)候,也就是老 VNode 節(jié)點(diǎn)的頭部與新 VNode 節(jié)點(diǎn)的尾部是同一節(jié)點(diǎn)的時(shí)候,將 oldStartVnode.elm
這個(gè)節(jié)點(diǎn)直接移動(dòng)到 oldEndVnode.elm
這個(gè)節(jié)點(diǎn)的后面即可。然后 oldStartIdx
向后移動(dòng)一位,newEndIdx
向前移動(dòng)一位。
同理,oldEndVnode
與 newStartVnode
符合 sameVnode
時(shí),也就是老 VNode 節(jié)點(diǎn)的尾部與新 VNode 節(jié)點(diǎn)的頭部是同一節(jié)點(diǎn)的時(shí)候,將 oldEndVnode.elm
插入到 oldStartVnode.elm
前面。同樣的,oldEndIdx
向前移動(dòng)一位,newStartIdx
向后移動(dòng)一位。
最后是當(dāng)以上情況都不符合的時(shí)候,這種情況怎么處理呢?
查找對比
那就是查找對比。
首先通過createKeyToOldIdx
方法生成oldVnode
的key
與 index
索引對應(yīng)的一個(gè) map
表。
然后我們根據(jù)newStartVnode.key
,可以快速地從 oldKeyToIdx
(createKeyToOldIdx
的返回值)中獲取相同 key
的節(jié)點(diǎn)的索引 idxInOld
,然后找到相同的節(jié)點(diǎn)。
這里又分三種情況
-
如果沒有找到相同的節(jié)點(diǎn),則通過
createElm
創(chuàng)建一個(gè)新節(jié)點(diǎn),并將newStartIdx
向后移動(dòng)一位。 -
如果找到了節(jié)點(diǎn),同時(shí)它符合
sameVnode
,則將這兩個(gè)節(jié)點(diǎn)進(jìn)行patchVnode
,將該位置的老節(jié)點(diǎn)賦值undefined
(之后如果還有新節(jié)點(diǎn)與該節(jié)點(diǎn)key相同可以檢測出來提示已有重復(fù)的 key ),同時(shí)將newStartVnode.elm
插入到oldStartVnode.elm
的前面。同理,newStartIdx
往后移動(dòng)一位。
-
如果不符合
sameVnode
,只能創(chuàng)建一個(gè)新節(jié)點(diǎn)插入到parentElm
的子節(jié)點(diǎn)中,newStartIdx
往后移動(dòng)一位。
添加、刪除節(jié)點(diǎn)
最后一步就很容易啦,當(dāng) while
循環(huán)結(jié)束以后,如果 oldStartIdx > oldEndIdx
,說明老節(jié)點(diǎn)比對完了,但是新節(jié)點(diǎn)還有多的,需要將新節(jié)點(diǎn)插入到真實(shí) DOM 中去,調(diào)用 addVnodes
將這些節(jié)點(diǎn)插入即可。
同理,如果滿足 newStartIdx > newEndIdx
條件,說明新節(jié)點(diǎn)比對完了,老節(jié)點(diǎn)還有多,將這些無用的老節(jié)點(diǎn)通過 removeVnodes
批量刪除即可。
總結(jié)
Diff算法是一種對比算法。對比兩者是舊虛擬DOM和新虛擬DOM
,對比出是哪個(gè)虛擬節(jié)點(diǎn)
更改了,找出這個(gè)虛擬節(jié)點(diǎn)
,并只更新這個(gè)虛擬節(jié)點(diǎn)所對應(yīng)的真實(shí)節(jié)點(diǎn)
,而不用更新其他數(shù)據(jù)沒發(fā)生改變的節(jié)點(diǎn),實(shí)現(xiàn)精準(zhǔn)
地更新真實(shí)DOM,進(jìn)而提高效率和性能
。
精準(zhǔn)
主要體現(xiàn)在,diff
算法首先就是找到可復(fù)用的節(jié)點(diǎn),然后移動(dòng)到正確的位置。當(dāng)元素沒有找到的話再來創(chuàng)建新節(jié)點(diǎn)。
擴(kuò)展
vue中為什么需要使用key,它的作用是什么?
key
是 Vue
中 vnode
的唯一標(biāo)記,通過這個(gè) key
,diff
操作可以更準(zhǔn)確、更快速。
- 更準(zhǔn)確:因?yàn)閹?
key
就不是就地復(fù)用了,在sameNode
函數(shù)a.key === b.key
對比中可以避免就地復(fù)用的情況。所以會(huì)更加準(zhǔn)確。 - 更快速:利用
key
的唯一性生成map
對象來獲取對應(yīng)節(jié)點(diǎn),比遍歷方式更快。
為什么不推薦使用index作為key
當(dāng)我們的列表只涉及到 展示,不涉及到排序、刪除、添加的時(shí)候使用index
作為key
是沒什么問題的。因?yàn)榇藭r(shí)的index
在每個(gè)元素上是唯一的。
但是如果涉及到排序、刪除、添加的時(shí)候就不能再使用index
作為key
了,因?yàn)槊總€(gè)元素key
不再唯一了。不唯一的key
,對diff
算法沒有任何幫助,寫和沒寫是一樣的。
(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)