八、组件更新
当完成了首次的渲染之后,组件的响应式数据发生了更新,再次触发了渲染watcher的getter,也就是调用了 vm._update(vm._render(), hydrating)调用update的这一过程就是组件更新的过程。_update函数首先通过const prevVnode = vm._vnode拿到之前定义的vnode,在之后的逻辑判断中prevVnode为true,接着执行 vm.$el = vm.__patch__(prevVnode, vnode),第一个参数传入之前的vnode,新的参数传入生成的新的vnode。vm.__patch__实际上是patch.js文件当中的patch函数。patch函数中,由于oldVnode定义了,所以本次会执行else逻辑。else逻辑中,首先通过oldVnode.nodeType拿到oldVnode的类型,以此来判断他是否是一个真实的元素节点,如果不是一个真实的元素节点,并且满足sameVnode(oldVnode, vnode),sameVnode函数会尝试拿到传入的两个vnode的key,key在写v-for的时候是非常常见的,如果他们的key相同(如果两者都不写key,则均为undefined,也满足相等的条件),如果满足,他会继续判断如果他们的tag相同,并且都是一个注释节点,并且data都是有定义的,并且是一个相同的input类型,或者如果满足 参数a是一个异步占位符节点并且,a.asyncFactory === b.asyncFactory并且b的执行是正确的,那么就返回true,否则返回false,也就是说samevnode判断两个新旧节点是否相同。如果满足上面的两个条件,那么他会执行patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly),如果新旧节点不同,他会执行else的逻辑,新旧节点不同的情况,他会分三步处理
- 第一步 创建新的节点
首先通过
oldVnode.elm拿到旧的节点,然后通过nodeOps.parentNode(oldElm)拿到旧节点的父级节点,在调用createElm方法创建新的dom节点。执行完这一步,会创建新的节点并进行插入,也就是新的节点和老的节点都存在于页面。 - 第二步 递归的更新父的占位符节点
首先他会判断是否有
vnode.parent,vnode.parent在_render的最后进行了定义,等于vm.$options._parentVnode也就是父的占位符节点。然后他会执行isPatchable(vnode),isPatchable函数会循环判断是否有vnode.componentInstance,如果有那么代表他是一个组件vnode,那么vnode = vnode.componentInstance._vnode,会继续去找他的渲染vnode,直到找到他的真实渲染节点,如果有父的占位符节点,执行destroy的钩子,然后通过ancestor.elm = vnode.elm对节点进行替换,这样他的父的占位符节点的引用,就指向了新的节点,然后判断如果是一个可挂载节点,那么去执行create等钩子,最后ancestor = ancestor.parent当ancestor.parent是存在的,那么他还是一个组件,所以会再向上去执行刚才的逻辑,形成递归的的更新父的占位符节点。 - 第三步 删除旧的节点 通过
removeVnodes([oldVnode], 0, 0)对旧的节点进行删除
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
...
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
...
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
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)
)
// update parent placeholder node element, recursively
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)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
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
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
如果sameVnode(oldVnode, vnode)为true,也就是他们的key相同,以及data相同等,则会执行patchVnode函数。
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
// 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
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]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} 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]
} 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 {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
- 仅仅文本的替换 我们假设这样一个场景
<template>
<div id='app'>
<div v-if="flag" @click="flag = false">123</div>
<div v-else @click="flag = true">444</div>
</div>
</template>
首次flag为true,当我点击div,触发flag=false,patchVnode函数会先定义oldCh和ch他们分别是旧的vnode和新的vnode的children,首次进入,最初会从<div id='app'>开始进行比较,他的children也就是数组中,有子元素div的vnode,此时的vnode是没有text的,接着判断,oldCh和ch都定义,则会执行updateChildren函数。updateChildren函数会先定义oldStartVnode旧vnode的children的开始节点(旧vnode的children数组的第一项),oldEndVnode旧vnode的children结束节点,newStartVnode新vnode的children开始节点,newEndVnode新vnode的children结束节点,然后他会判断oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,因为我们的#app.div下此时新旧都为div,则这4个值均为0,首先他会判断是否未定义 if (isUndef(oldStartVnode)),此时不满足,接着执行else if (isUndef(oldEndVnode))也不满足,然后他会比较else if (sameVnode(oldStartVnode, newStartVnode)),此时两者是满足samevnode的,会执行patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);,再次执行patchVnode,他的oldVnode和vnode则为这两个新旧div的vnode,同样,他们也会先定义自己的children,也就是两个文本节点123和456的vnode,那么此时div也是没有text的,同时两者都有children,则再次执行updateChildren,再次走入逻辑判断,直到走到之前的samevnode处,接着他会再去执行patchVnode,这次,两个文本vnode的children为undefined,同时vnode.text不为空,接着判断else if (oldVnode.text !== vnode.text)两者的text一个为123,一个为444,满足此条件,接着执行nodeOps.setTextContent(elm, vnode.text)进行文本的替换。此时递归执行完毕,回到最近一次调用updateChildren的场景,也就是两个#app.div下的两个div的updateChildren,执行oldStartVnode = oldCh[++oldStartIdx] oldStartVnode 则为undefined,newStartVnode = newCh[++newStartIdx],newStartVnode也为undefined,最后的两个判断oldStartIdx > oldEndIdx和newStartIdx > newEndIdx均不满足,则结束执行,#app.div的updateChildren同理。
- 数组的push操作 我们假设这样一个场景
<template>
<div id="app">
<ul>
<li v-for="item in arr" :key="item.id">{{ item.text }}</li>
</ul>
<button @click="arr.push({ id: 3, text: 3 })">添加</button>
</div>
</template>
<script>
export default {
data() {
return {
arr: Array.from({ length: 3 }).map((item, index) => ({
id: index,
text: index
}))
}
}
}
</script>
此时页面中ul的子元素有3个li,li的key为0,1,2,div里的文本内容也为0,1,2。当点击button,往arr中push一个{ id: 3, text: 3 },进入ul的updateChildren函数,此时oldCh为3个li的vnode节点,而newCh为4个li的的vnode节点。也就是oldEndIdx为3,newEndIdx为4,此时先判断(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)此时为 (0<=2)&&(0<=3)为true,接着判断oldStartVnode是定义的,oldEndVnode也是定义的,当判断else if (sameVnode(oldStartVnode, newStartVnode))是成立的,则执行oldStartVnode和newStartVnode的patchVnode,旧的vnode的key为0的li和新的vnode的key为0的li,他们的文本节点是相同,则当执行带他们的patchVnode的时候,什么也不会执行。接着返回ul的updateChildren函数,执行 oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]也就是oldStartIdx由0变为了1,oldStartVnode指向了第二个li,newStartIdx和newStartVnode同理。接着再次判断 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) 1<=3&&1<=4也成立,也就是说,直到执行到第3个li的对比,两者并无区别,什么操作也没做。对比完第三个li,此时oldStartIdx为3,oldStartVnode为undefined,newStartIdx也为3,newStartVnode为新创建的li key为3的vnode节点。此时while条件中的oldStartIdx <= oldEndIdx不成立,则接着向下执行,判断if (oldStartIdx > oldEndIdx)此时oldStartIdx为3,oldEndIdx为2,则成立,对剩余的接着去执行了addVnodes的插入操作
-
数组的pop操作 之前的操作都是相同的,当执行完ul的updateChildren的while后,newStartIdx为3,newEndIdx为2,则会执行
removeVnodes操作,移除多余的vnode节点。 -
数组的reverse操作 会执行到判断
else if(sameVnode(oldStartVnode, newEndVnode)),接着执行nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))把第一个节点插入到最后,也就是由 0,1,2变为了1,2,0的顺序。接着让oldStartIdx变为了1,oldStartVnode也指向他,newEndIdx变为1,newEndVnode指向他。此时两者都指向了key为1的li,接着就满足了else if (sameVnode(oldStartVnode, newEndVnode)),继续把key为1的li插入到了key为0的li之前,也就是形成了 2,1,0的最终结果
可以看到对于相同节点的diff,会递归向下比较,而不是会直接进行全部的删除和重新创建,这也是vnode做的一层优化处理