原文链接我的blog,欢迎STAR。
接着上一篇,我们继续来讲Vue的Virtual Dom
diff
算法中的patchVnode
方法,以及核心updateChildren
方法。
在上篇中,我们谈到,当vnode不为真实节点,且vnode与oldVnode为同一节点时,会调用patchVnode方法。
我们直接从源码上进行分析:
// patchVnode()有四个参数
// oldVnode: 旧的虚拟节点
// vnode: 新的虚拟节点
// insertedVnodeQueue: 存在于整个patch中,用于收集patch中插入的vnode;
// removeOnly: 这个在源码里有提到,removeOnly is a special flag used only by<transition-group>也就是说是特殊的flag,用于transition-group组件。
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果oldVnode与vnode为同一引用, 不进行任何处理。
if (oldVnode === vnode) {
return
}
// 如果不为同一引用,那说用新的vnode创建了。
// 如果vnode, oldVnode都为静态节点,且vnode.key === oldVnode.key相等时,当vnode为克隆节点,或者vnode有v-once指令时,只需把oldVnode对应的真实dom,以及组件实例都复制到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
// 在进行下一步操作之前会调用prepatch hook,但是这个是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引用到现在的真实DOM,当elm修改的时候,会同步修改vnode.elm
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 我们先patchVnode, 方法就是先调用全局的update hook
// 然后调用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未定义
// 这里有个值得注意的地方,具有text属性的vnode不应该具备有children
// 对于<p>abc<i>123</i></p>的写法应该是
// h('p', ['abc', h('i', '123')])
// 而不是, h('p', 'abc', [h('i', '123')])
// 因此,对text存在与否的情况需单独拿出来分析
if (isUndef(vnode.text)) {
// 如果oldVnode与vnode都存在children
if (isDef(oldCh) && isDef(ch)) {
// 如果两个children 不相同,调用updateChildren()方法更新子节点的操作。(接下来将讲解)
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果只有vnode.children 存在
// 当oldVnode.text不为空,vnode.text未定义时,清空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是一个text node),且不等于oldVnode.text
// 更新elm.textContent
nodeOps.setTextContent(elm, vnode.text)
}
// 最后再调用 postpatch hook。
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
接着说重点 当oldVnode.children与vnode.children都存在,且不相同时调用的updateChildren()
方法, 同样的,咱们从源码上分析:
// updateChildren(),有五个参数
// parentElm: oldVnode.elm 的引用
// oldCh, newCh: 分别是上面分析中的oldVnode.children, vnode.children
// insertedVnodeQueue, removeOnly 请参考上面。
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种情况
// 比较判断的依据是,sameVnode(),值不得值得比较。
// key,tag(当前节点标签名),isComment(是否是注释节点)
// data,节点的数据对象是否都存在或都不存在
// (a, b)=> {
// return (
// a.key === b.key &&
// a.tag === b.tag &&
// a.isComment === b.isComment &&
// isDef(a.data) === isDef(b.data) &&
// sameInput(a, b)
// )
// }
// 当oldStartIndex > oldEndIdx 或者 newStartIndex > newEndIdx, 停止遍历。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 对于vnode.key的比较,会把oldVnode = null
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
// 同上
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 第一种情况:
// 从oldCh与newCh的第一个开始,逐步往后遍历。
// 如果oldStartVnode与newStartVnode值得比较,
// 执行pathchVnode()方法
// oldStartVnode, newStartVnode相对位置不变。
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 第二种情况:
// 从oldCh与newCh的最后一个开始,逐步往前遍历。
// 如果oldEndVnode,newEndVnode值得比较
// 执行pathchVnode()
// oldEndVnode, newEndVnode相对位置不变
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 第三种情况:
// 从oldCh的第一个,newCh的最后一个开始,oldCh往后,newCh往前遍历,
// 如果oldStartVnode与newEndVnode值得比较
// 此时需要把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的最后一个,newCh的第一个,oldCh往前,newCh往后,遍历。
// 如果oldEndVnode与newStartVnode值得比较
// 此时需要把oldEndVnode放到oldStartVnode前边
// oldCh往前,newCh往后
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 第五种情况:
// 使用key比较
// 首先会调用createKytoOldIdx()方法,产生一个key-index对象列表
// 然后根据这个表来进行更改
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 如果newStartVnode.key存在,根据key来找到对应的index,命名为idxInOld
// 如果不存在,设置为null
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
// 如果idxInOld不存在时,此时是一个新的vnode
// 将这个vnode插入到oldStartVnode.elm 的前边
// 把newStartVnode设置为下一个节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// 如果idxInOld存在时,那么对应的oldVnode存在
// 根据index,找到oldVnode对应的children
elmToMove = oldCh[idxInOld]
// 如果不是生产环境,且elmToMove不存在
// 此时因为idxInOld已经存在,而oldCh[idxInOld]不存在
// 只有可能keys重复了
/* 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.'
)
}
// 如果根据vnode.key找出的elmToMove与newStartVnode值得比较比较
// patchVnode这两个节点
// 之后,需要把这个child设置为undefined
// 同时需要把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]**重点内容**
} else {
// same key but different element. treat as new element
// 如果不值得比较,此时key已经相同,说明是tag不同,或者其他不同,此时创建一个新节点
// 将这个vnode插入到oldStartVnode.elm 的前边
// 把newStartVnode设置为下一个节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
// 遍历完成之后,存在两种情况
// 如果 oldStartIdx > oldEndIdx, 即oldCh先遍历完
// 位于 newStartIdx与newEndIdx之间的节点都可认为是新的节点
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先遍历完
// 此时,位于oldStartIdx与oldEndIdx之间的节点已经不存在了
// 调用removeVnodes()方法移除节点。
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
直接在源码上分析,可能有点乱,总结一下:
patchVnode
共有以下情况:
如果
oldVnode
与vnode
引用完全一致,则可以认为没有变化,无需进行任何操作。如果
vnode
,oldVnode
都为静态节点,且vnode.key === oldVnode.key
相等时,当vnode
为克隆节点,或者vnode
有v-once
指令时,只需把oldVnode
对应的真实dom,以及组件实例都复制到vnode
上。-
如果
vnode
不是text node
:如果
vnode.children
与oldVnode.children
都存在,调用updateChildren()
方法。当
vnode.children
存在,oldVnode.children
不存在时,添加vnode.children
。当
vnode.children
不存在,oldVnode.children
存在时,需要移除oldVnode.children
。当两者的
children
都不存在时,如果oldVnode
为text node
,则需清空elm.textContent
。
如果
vnode
是text node
,改变elm.textContent
。
patchVnode
有一个值得注意的地方是,vdom
中规定,具有text
属性的vnode
不应该具备children
,因此需把text node
单独拿出来分析。
updateChildren()
方法共有5种比较方式,前四种无key的情况,后一种为有key的情况,当oldStartIdx > oldEndIdx
或者newStartIdx > newOldStartIdx
的时候停止遍历。
引用推荐的那篇文章图:
第一种比较方式从
oldCh
与newCh
各自第一个vnode
开始比较,当值得比较时,调用上述中的patchVnode
方法进行比较, 同时将oldCh
与newCh
的下一个vnode
分别设为oldStartVnode
与newStartVnode
(比较的相对位置不变),startVnode
既是开始比较的vnode
。第二种比较方式从
oldCh
与newCh
各自最后一个vnode
开始比较,当值得比较时,调用上述中的patchVnode
方法进行比较,同时将oldCh
与newCh
的上一个vnode
分别设置为oldEndVnode
与newEndVnode
(比较的相对位置不变),,endVnode
既是结束比较的vnode
。第三种比较方式,从
oldCh
的第一个vnode
与newCh
的最后一个vnode
开始比较,当值得比较时,调用上述中的patchVnode
方法比较,同时将oldCh
的下一个vnode
设置为oldStartVnode
,将newCh
的上一个vnode
设置为newEndVnode
,并且此时说明oldStartVnode.elm
向右移动,并且已经移动到oldEndVnode.elm
的后边了,调用相应的方法移动位置。第四种比较方式,从
oldCh
的最后一个vnode
与newCh
的第一个vnode
开始比较,当值得比较时,调用上述中的patchVnode
方法比较,同时将oldCh
的上一个vnode
设置为oldEndVnode
,将newCh
的上一个vnode
设置为newStartVnode
,并且此时说明oldEndVnode.elm
向左移动,并且已经移动到oldStartVnode.elm
的前边了,调用相应的方法移动位置。-
第五种,使用key比较,先会产生一个key-index表,然后判断vnode.key存在与否?
如果不存在,是一个新的vnode,将这个vnode插入到oldStartVnode.elm 的前边,并且把newStartVnode设置为下一个节点。
-
如果存在,那么对应的
oldVnode
应该存在,此时可以根据key来找到对应的vnode
,然后判断这个vnode
与newStartVnode
是否值得比较?当值得比较时,调用
patchVnode
,并且需要把这个child设置为undefined,同时需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影响接下来的遍历。如果不值得比较,此时key已经相同,说明是tag不同,或者其他不同,此时创建一个新节点将这个vnode插入到oldStartVnode.elm 的前边。
遍历完成后,如果oldCh先遍历完,位于newStartIdx与newEndIdx之间的节点都可认为是新的节点,调用相应的方法插入节点。如果newCh先遍历完,此时,位于oldStartIdx与oldEndIdx之间的节点已经不存在了,调用removeVnodes()方法移除节点。
结语
码完这篇历时四天的文章,我的身心是崩溃的,期间查阅了相当多的资料,来确保表达的准确性(当然其中还有一些错误的地方,如有发现,请指出。),推荐从关于一些Vue的文章。(2),开始阅读,算是逐渐深入吧,从render,template,el => vnode => diff算法。如果各位同学喜欢,麻烦点个赞。谢谢。
完。