关于一些Vue的文章。(4)

时间:2023-02-02 14:43:35

原文链接我的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共有以下情况:

  • 如果oldVnodevnode引用完全一致,则可以认为没有变化,无需进行任何操作。

  • 如果vnode, oldVnode都为静态节点,且vnode.key === oldVnode.key相等时,当vnode为克隆节点,或者vnodev-once指令时,只需把oldVnode对应的真实dom,以及组件实例都复制到vnode上。

  • 如果vnode不是text node:

    • 如果vnode.childrenoldVnode.children都存在,调用updateChildren()方法。

    • vnode.children存在,oldVnode.children不存在时,添加vnode.children

    • vnode.children不存在,oldVnode.children存在时,需要移除oldVnode.children

    • 当两者的children都不存在时,如果oldVnodetext node,则需清空elm.textContent

  • 如果vnodetext node,改变elm.textContent

patchVnode有一个值得注意的地方是,vdom中规定,具有text属性的vnode不应该具备children,因此需把text node单独拿出来分析。


updateChildren()方法共有5种比较方式,前四种无key的情况,后一种为有key的情况,当oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的时候停止遍历。

引用推荐的那篇文章图:

关于一些Vue的文章。(4)

  • 第一种比较方式从oldChnewCh各自第一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法进行比较, 同时将oldChnewCh的下一个vnode分别设为oldStartVnodenewStartVnode(比较的相对位置不变), startVnode既是开始比较的vnode

  • 第二种比较方式从oldChnewCh各自最后一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法进行比较,同时将oldChnewCh的上一个vnode分别设置为oldEndVnodenewEndVnode(比较的相对位置不变),, endVnode既是结束比较的vnode

  • 第三种比较方式,从oldCh的第一个vnodenewCh的最后一个vnode开始比较,当值得比较时,调用上述中的patchVnode方法比较,同时将oldCh的下一个vnode设置为oldStartVnode,将newCh的上一个vnode设置为newEndVnode,并且此时说明oldStartVnode.elm向右移动,并且已经移动到oldEndVnode.elm的后边了,调用相应的方法移动位置。

  • 第四种比较方式,从oldCh的最后一个vnodenewCh的第一个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,然后判断这个vnodenewStartVnode是否值得比较?

      • 当值得比较时,调用patchVnode,并且需要把这个child设置为undefined,同时需要把oldStartVnode.elm的位置移到newStartVnode.elm之前,以免影响接下来的遍历。

      • 如果不值得比较,此时key已经相同,说明是tag不同,或者其他不同,此时创建一个新节点将这个vnode插入到oldStartVnode.elm 的前边。

遍历完成后,如果oldCh先遍历完,位于newStartIdx与newEndIdx之间的节点都可认为是新的节点,调用相应的方法插入节点。如果newCh先遍历完,此时,位于oldStartIdx与oldEndIdx之间的节点已经不存在了,调用removeVnodes()方法移除节点。

完。