前言
上一篇《vue异步更新流程梳理》 梳理了数据从赋值到更新到视图的整体流程。但是最后的步骤vm._update(vm._render()) 只是粗略的提了一嘴,现在就仔细的研究它内部的细节,搞清楚patch流程和diff原理是我们看源码的重中之重。
更新过程
- 代码
Document
{{value}}
-
在vue实例调用$mount的时候,就已经把updateComponent 方法通过new Watcher(vm, updateComponent)传入到渲染watcher里面, 且挂在watcher.getter上,得到一个渲染watcher, 渲染watcher在以后每次响应式数据更新都会执行watcher.getter 即 updateComponent 方法。
当更新数据的时候就会执行这个updateComponent方法,即方法里面的vm._update(vm._render()),vm.render() 得到一个vnode,那么vm._update到底干什么? 进去看看
其实就是判断是进行初始化流程(initial render) 或者是更新流程(updates),但是都是调用了 vm.__patch__ 函数;两者的区别是
- 初始化的第一个参数是el节点,也就是例子中真实的dom节点#app
- 而更新流程的第一个参数就是prevVnode, 这个prevVnode其实是一个vnode, 是更新前的vnode
至此,无论是初始化还是更新都是靠patch来完成的 ,我们只需要看update流程就可以了。进入patch内部
patch函数主要接收oldVnode 与 vnode两个参数,其实就是新旧两棵虚拟树。这里经过判断条件 !isRealElement && sameVnode(oldVnode, vnode),不是真实节点 且是相同的vnode,进入patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); 我们只要关注oldVnode, vnode这两个参数即可。
按照我们的例子,此时的oldVnode 与 vnode分别是
oldVnode: {
tag: 'div',
key: undefined,
elm: div#app, // 这是该vnode节点映射对应的真实dom节点
children: [
{
tag: undefined,
key: undefined,
elm: text // 文本节点
text: '1'
}
]
}
vnode: {
tag: 'div',
key: undefined,
elm: div#app, // 这是该vnode节点映射对应的真实dom节点
children: [
{
tag: '',
key: undefined,
elm: undefined, // 注意这里是undefined,此时还没有diff
text: '2'
}
]
}
此处只列出关键属性tag, key, elm, children,elm,还有很多其他的属性没有列出。真实的虚拟树节点应该是如下图
我们能看出 两个vnode之间就是children[0]的不同:
oldVnode.children[0].text === '1',
vnode.children[0].text === '2'
追踪流程发现,我们进入oldVnode 与 vnode的children进行对比,在updateChildren函数中。
我们先不去看updateChildren的逻辑,继续看patchVnode这个函数其他的逻辑分支,得出oldVnode 与 vnode的对比流程:
- 拿出两者的children: oldCh, ch
- 如果vnode是元素节点
2.1,如果oldCh, ch两者都存在且不相同,进入updateChildren流程
2.2,否则如果只是ch 存在,oldCh不存在,那么直接操作真实dom, 添加ch节点
2.3,否则如果只是oldCh 存在,ch不存在,那么直接操作真实dom, 删除oldCh节点
2.4,否则如果只是oldCh是文本, ch不存在,直接操作真实dom设置文本节点为空 '' - 如果vnode是文本节点,直接操作真实dom, 设置文本节点为 vnode.text
总结:patchVnode这个方法的主要作用是对比两个虚拟节点过程中去更新真实dom
接下来我们进入updateChildren流程,这是两个children的对比,看一下这个函数的定义
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
// 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)
}
}
函数解读:
- 参数 parentElm:真实的dom元素,做为父节点,供更新children时去插入
- 参数 oldCh:老的虚拟树的children
- 参数 newCh: 新的虚拟树的children
- oldStartIdx:老数组的开始的下标index
- newStartIdx: 新数组的开始的下标index
- oldEndIdx: 老数组的结束的下标index
- oldStartVnode : 老数组的开始的节点
- oldEndVnode : 老数组的结束的节点
- newEndIdx :新数组的结束的下标index
- newStartVnode :新数组的开始的节点
- newEndVnode :新数组的结束的节点
- oldKeyToIdx:老数组的节点的key与index的一个映射map
- idxInOld:老数组的某个节点的index
- vnodeToMove:待移动的节点,也就是在oldKeyToIdx中找到key的可复用的节点
- refElm: 一个真实的节点,用来做参考位置的,表示从哪里开始添加新的dom节点
下面是两个数组进行diff的流程,也就是diff算法
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]
}
}
diff解读:
新旧两个数组,都有双端指针,两端指针向中间靠拢,直到某个数组的两端指针相交则退出循环。
在这个过程中,会先判断是否有以下四种情况
- 旧首 - 新首 是同一个节点,互相比较,深度递归进行patchVnode流程,
- 旧尾 - 新尾 是同一个节点,互相比较,深度递归进行patchVnode流程
- 旧首 - 新尾 是同一个节点,互相比较,深度递归进行patchVnode流程
- 旧尾 - 新首 是同一个节点,互相比较,深度递归进行patchVnode流程
如果不符合这4种情况,那就基于旧数组遍历一次,拿到每个节点的key和index,就是oldKeyToIdx: {key1: 0, key2: 1}这种情况。然后去新数组首个节点开始匹配,匹配到就进行递归patchVnode流程,没匹配到就进行创建新节点,插入到真实dom节点里面去。
当循环结束,此时要么是旧数组相交,要么是新数组相交,只有这两种情况:
- 旧数组相交,说明新数组还没有相交,那么要根据相交的位置插入新数组剩余的未遍历到节点
- 新数组相交,说明旧数组还没有相交,那么要删除旧数组剩余的未遍历到的节点
至此diff流程结束。
总结
两个虚拟树进行对比:
patch(oldVnode, vnode) -> patchVnode(oldVnode, vnode) -> updataChildren(oldCh, newCh)
在updataChildren(oldCh, newCh)的过程中也会进行 patchVnode(oldVnode, vnode) ,如此虚拟树深度优先递归diff完成。
更加详细直观的图看此链接
https://www.processon.com/view/5e809004e4b08e4e2447d02e