八、组件更新
当完成了首次的渲染之后,组件的响应式数据发生了更新,再次触发了渲染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做的一层优化处理
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!