原文鏈接我的blog,歡迎STAR。
接著上一篇,我們繼續(xù)來講Vue的Virtual Dom diff 算法中的patchVnode方法,以及核心updateChildren方法。
在上篇中,我們談到,當(dāng)vnode不為真實(shí)節(jié)點(diǎn),且vnode與oldVnode為同一節(jié)點(diǎn)時(shí),會(huì)調(diào)用patchVnode方法。
我們直接從源碼上進(jìn)行分析:
// patchVnode()有四個(gè)參數(shù)
// oldVnode: 舊的虛擬節(jié)點(diǎn)
// vnode: 新的虛擬節(jié)點(diǎn)
// insertedVnodeQueue: 存在于整個(gè)patch中,用于收集patch中插入的vnode;
// removeOnly: 這個(gè)在源碼里有提到,removeOnly is a special flag used only by<transition-group>也就是說是特殊的flag,用于transition-group組件。
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果oldVnode與vnode為同一引用, 不進(jìn)行任何處理。
if (oldVnode === vnode) {
return
}
// 如果不為同一引用,那說用新的vnode創(chuàng)建了。
// 如果vnode, oldVnode都為靜態(tài)節(jié)點(diǎn),且vnode.key === oldVnode.key相等時(shí),當(dāng)vnode為克隆節(jié)點(diǎn),或者vnode有v-once指令時(shí),只需把oldVnode對(duì)應(yīng)的真實(shí)dom,以及組件實(shí)例都復(fù)制到vnode上。
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
vnode.elm = oldVnode.elm
vnode.componentInstance = oldVnode.componentInstance
return
// 在進(jìn)行下一步操作之前會(huì)調(diào)用prepatch hook,但是這個(gè)是vnode在data里定義的prepatch hook,并不是全局定義的prepatch hook
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 讓vnode引用到現(xiàn)在的真實(shí)DOM,當(dāng)elm修改的時(shí)候,會(huì)同步修改vnode.elm
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 我們先patchVnode, 方法就是先調(diào)用全局的update hook
// 然后調(diào)用data里定義的update hook
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)
}
// 如果vnode.text未定義
// 這里有個(gè)值得注意的地方,具有text屬性的vnode不應(yīng)該具備有children
// 對(duì)于<p>abc<i>123</i></p>的寫法應(yīng)該是
// h('p', ['abc', h('i', '123')])
// 而不是, h('p', 'abc', [h('i', '123')])
// 因此,對(duì)text存在與否的情況需單獨(dú)拿出來分析
if (isUndef(vnode.text)) {
// 如果oldVnode與vnode都存在children
if (isDef(oldCh) && isDef(ch)) {
// 如果兩個(gè)children 不相同,調(diào)用updateChildren()方法更新子節(jié)點(diǎn)的操作。(接下來將講解)
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果只有vnode.children 存在
// 當(dāng)oldVnode.text不為空,vnode.text未定義時(shí),清空elm.textContent
// 添加vnode.children
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果只有oldVnode.children存在,移除oldVnode.children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 同上,如果oldVnode.text存在,vnode.text不存在,清空elm.textContent
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果vnode.text存在(vnode是一個(gè)text node),且不等于oldVnode.text
// 更新elm.textContent
nodeOps.setTextContent(elm, vnode.text)
}
// 最后再調(diào)用 postpatch hook。
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
接著說重點(diǎn) 當(dāng)oldVnode.children與vnode.children都存在,且不相同時(shí)調(diào)用的updateChildren()方法, 同樣的,咱們從源碼上分析:
// updateChildren(),有五個(gè)參數(shù)
// parentElm: oldVnode.elm 的引用
// oldCh, newCh: 分別是上面分析中的oldVnode.children, vnode.children
// insertedVnodeQueue, removeOnly 請(qǐng)參考上面。
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, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 遍歷過程共有5種情況
// 比較判斷的依據(jù)是,sameVnode(),值不得值得比較。
// key,tag(當(dāng)前節(jié)點(diǎn)標(biāo)簽名),isComment(是否是注釋節(jié)點(diǎn))
// data,節(jié)點(diǎn)的數(shù)據(jù)對(duì)象是否都存在或都不存在
// (a, b)=> {
// return (
// a.key === b.key &&
// a.tag === b.tag &&
// a.isComment === b.isComment &&
// isDef(a.data) === isDef(b.data) &&
// sameInput(a, b)
// )
// }
// 當(dāng)oldStartIndex > oldEndIdx 或者 newStartIndex > newEndIdx, 停止遍歷。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 對(duì)于vnode.key的比較,會(huì)把oldVnode = null
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
// 同上
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 第一種情況:
// 從oldCh與newCh的第一個(gè)開始,逐步往后遍歷。
// 如果oldStartVnode與newStartVnode值得比較,
// 執(zhí)行pathchVnode()方法
// oldStartVnode, newStartVnode相對(duì)位置不變。
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 第二種情況:
// 從oldCh與newCh的最后一個(gè)開始,逐步往前遍歷。
// 如果oldEndVnode,newEndVnode值得比較
// 執(zhí)行pathchVnode()
// oldEndVnode, newEndVnode相對(duì)位置不變
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 第三種情況:
// 從oldCh的第一個(gè),newCh的最后一個(gè)開始,oldCh往后,newCh往前遍歷,
// 如果oldStartVnode與newEndVnode值得比較
// 此時(shí)需要把oldStartVnode放到oldEndVnode后面
// oldCh往后,newCh往前
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)) {
// 第四種情況:
// 從oldCh的最后一個(gè),newCh的第一個(gè),oldCh往前,newCh往后,遍歷。
// 如果oldEndVnode與newStartVnode值得比較
// 此時(shí)需要把oldEndVnode放到oldStartVnode前邊
// oldCh往前,newCh往后
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 第五種情況:
// 使用key比較
// 首先會(huì)調(diào)用createKytoOldIdx()方法,產(chǎn)生一個(gè)key-index對(duì)象列表
// 然后根據(jù)這個(gè)表來進(jìn)行更改
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 如果newStartVnode.key存在,根據(jù)key來找到對(duì)應(yīng)的index,命名為idxInOld
// 如果不存在,設(shè)置為null
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
// 如果idxInOld不存在時(shí),此時(shí)是一個(gè)新的vnode
// 將這個(gè)vnode插入到oldStartVnode.elm 的前邊
// 把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// 如果idxInOld存在時(shí),那么對(duì)應(yīng)的oldVnode存在
// 根據(jù)index,找到oldVnode對(duì)應(yīng)的children
elmToMove = oldCh[idxInOld]
// 如果不是生產(chǎn)環(huán)境,且elmToMove不存在
// 此時(shí)因?yàn)閕dxInOld已經(jīng)存在,而oldCh[idxInOld]不存在
// 只有可能keys重復(fù)了
/* 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.'
)
}
// 如果根據(jù)vnode.key找出的elmToMove與newStartVnode值得比較比較
// patchVnode這兩個(gè)節(jié)點(diǎn)
// 之后,需要把這個(gè)child設(shè)置為undefined
// 同時(shí)需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影響接下來的遍歷。
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]**重點(diǎn)內(nèi)容**
} else {
// same key but different element. treat as new element
// 如果不值得比較,此時(shí)key已經(jīng)相同,說明是tag不同,或者其他不同,此時(shí)創(chuàng)建一個(gè)新節(jié)點(diǎn)
// 將這個(gè)vnode插入到oldStartVnode.elm 的前邊
// 把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
// 遍歷完成之后,存在兩種情況
// 如果 oldStartIdx > oldEndIdx, 即oldCh先遍歷完
// 位于 newStartIdx與newEndIdx之間的節(jié)點(diǎn)都可認(rèn)為是新的節(jié)點(diǎn)
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 如果newStartIdx > newEndIdx, 即newCh先遍歷完
// 此時(shí),位于oldStartIdx與oldEndIdx之間的節(jié)點(diǎn)已經(jīng)不存在了
// 調(diào)用removeVnodes()方法移除節(jié)點(diǎn)。
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
直接在源碼上分析,可能有點(diǎn)亂,總結(jié)一下:
patchVnode共有以下情況:
如果
oldVnode與vnode引用完全一致,則可以認(rèn)為沒有變化,無需進(jìn)行任何操作。如果
vnode,oldVnode都為靜態(tài)節(jié)點(diǎn),且vnode.key === oldVnode.key相等時(shí),當(dāng)vnode為克隆節(jié)點(diǎn),或者vnode有v-once指令時(shí),只需把oldVnode對(duì)應(yīng)的真實(shí)dom,以及組件實(shí)例都復(fù)制到vnode上。-
如果
vnode不是text node:如果
vnode.children與oldVnode.children都存在,調(diào)用updateChildren()方法。當(dāng)
vnode.children存在,oldVnode.children不存在時(shí),添加vnode.children。當(dāng)
vnode.children不存在,oldVnode.children存在時(shí),需要移除oldVnode.children。當(dāng)兩者的
children都不存在時(shí),如果oldVnode為text node,則需清空elm.textContent。
如果
vnode是text node,改變elm.textContent。
patchVnode有一個(gè)值得注意的地方是,vdom中規(guī)定,具有text屬性的vnode不應(yīng)該具備children,因此需把text node單獨(dú)拿出來分析。
updateChildren()方法共有5種比較方式,前四種無key的情況,后一種為有key的情況,當(dāng)oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的時(shí)候停止遍歷。
引用推薦的那篇文章圖:

第一種比較方式從
oldCh與newCh各自第一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法進(jìn)行比較, 同時(shí)將oldCh與newCh的下一個(gè)vnode分別設(shè)為oldStartVnode與newStartVnode(比較的相對(duì)位置不變),startVnode既是開始比較的vnode。第二種比較方式從
oldCh與newCh各自最后一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法進(jìn)行比較,同時(shí)將oldCh與newCh的上一個(gè)vnode分別設(shè)置為oldEndVnode與newEndVnode(比較的相對(duì)位置不變),,endVnode既是結(jié)束比較的vnode。第三種比較方式,從
oldCh的第一個(gè)vnode與newCh的最后一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法比較,同時(shí)將oldCh的下一個(gè)vnode設(shè)置為oldStartVnode,將newCh的上一個(gè)vnode設(shè)置為newEndVnode,并且此時(shí)說明oldStartVnode.elm向右移動(dòng),并且已經(jīng)移動(dòng)到oldEndVnode.elm的后邊了,調(diào)用相應(yīng)的方法移動(dòng)位置。第四種比較方式,從
oldCh的最后一個(gè)vnode與newCh的第一個(gè)vnode開始比較,當(dāng)值得比較時(shí),調(diào)用上述中的patchVnode方法比較,同時(shí)將oldCh的上一個(gè)vnode設(shè)置為oldEndVnode,將newCh的上一個(gè)vnode設(shè)置為newStartVnode,并且此時(shí)說明oldEndVnode.elm向左移動(dòng),并且已經(jīng)移動(dòng)到oldStartVnode.elm的前邊了,調(diào)用相應(yīng)的方法移動(dòng)位置。-
第五種,使用key比較,先會(huì)產(chǎn)生一個(gè)key-index表,然后判斷vnode.key存在與否?
如果不存在,是一個(gè)新的vnode,將這個(gè)vnode插入到oldStartVnode.elm 的前邊,并且把newStartVnode設(shè)置為下一個(gè)節(jié)點(diǎn)。
-
如果存在,那么對(duì)應(yīng)的
oldVnode應(yīng)該存在,此時(shí)可以根據(jù)key來找到對(duì)應(yīng)的vnode,然后判斷這個(gè)vnode與newStartVnode是否值得比較?當(dāng)值得比較時(shí),調(diào)用
patchVnode,并且需要把這個(gè)child設(shè)置為undefined,同時(shí)需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影響接下來的遍歷。如果不值得比較,此時(shí)key已經(jīng)相同,說明是tag不同,或者其他不同,此時(shí)創(chuàng)建一個(gè)新節(jié)點(diǎn)將這個(gè)vnode插入到oldStartVnode.elm 的前邊。
遍歷完成后,如果oldCh先遍歷完,位于newStartIdx與newEndIdx之間的節(jié)點(diǎn)都可認(rèn)為是新的節(jié)點(diǎn),調(diào)用相應(yīng)的方法插入節(jié)點(diǎn)。如果newCh先遍歷完,此時(shí),位于oldStartIdx與oldEndIdx之間的節(jié)點(diǎn)已經(jīng)不存在了,調(diào)用removeVnodes()方法移除節(jié)點(diǎn)。
結(jié)語
碼完這篇?dú)v時(shí)四天的文章,我的身心是崩潰的,期間查閱了相當(dāng)多的資料,來確保表達(dá)的準(zhǔn)確性(當(dāng)然其中還有一些錯(cuò)誤的地方,如有發(fā)現(xiàn),請(qǐng)指出。),推薦從關(guān)于一些Vue的文章。(2),開始閱讀,算是逐漸深入吧,從render,template,el => vnode => diff算法。如果各位同學(xué)喜歡,麻煩點(diǎn)個(gè)贊。謝謝。
完。