在分析前首先提出几个问题:
基于 Vue.js v2.6.10 进行分析
patch 方法什么时候、在什么地方调用?
组件挂载和更新时会调用 updateComponent 方法,updateComponent 中会调用组件实例上的 _update 方法,_update 方法中会对组件实例上的 __patch__ 方法进行调用。_update 的第一个参数为调用组件实例 render 方法生成的新的 Vnode 节点,在 _update 方法内,可以通过组件实例获取当前的(旧) Vnode 节点, 如果旧 Vnode 节点不存在,则说明为初始渲染,那么 __patch__ 方法的第一个参数就是组件实例 $el 属性绑定的元素,否则为旧的 Vnode 节点,__patch__ 的第二个参数为新的 Vnode 节点。而 __patch__ 方法是 patch 方法的别名。
patch 方法主要做了什么?
patch 方法通过对 vnode oldVnode 进行判断来执行不同的操作:
oldVnode不存在, 根据vnode创建dom元素oldVnode为dom元素或与vnode不为sameVnode,则销毁oldVnode,根据vnode生成dom元素oldVnodevnode为sameVnode且oldVnode不为dom元素, 则执行patchVnode方法
patch 方法源码分析如下:
- 首选判断
vnode节点是否存在,如果不存在vnode节点 ,说明需要销毁当前oldVnode节点,然后退出patch过程1
2
3
4if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
} - 然后判断
oldVnode是否存在, 如果oldVnode不存在,说明是初始patch,则根据vnode生成相应的元素,最后返回生成的元素退出patch过程1
2
3
4
5
6
7
8if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm - 如果
oldVnode存在, 在oldVnode不为dom元素且与sameVnode(oldVnode, vnode)返回真的情况下,执行patchVnode方法1
2
3
4if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} - 如果第三步为假,则根据
vnode生成相应的元素,并删除oldVnode,然后退出patch过程
什么情况下两个 vnode 节点是 sameVnode ?
基本要求:两个节点的 key 相同,如果两个 vnode 节点的 key 都为 undefined,则条件也成立
1 | return ( |
Vnode 是如何进行新老节点比对、更新的?
- 通过
patchVnode进行新,旧节点比对、更新 - 通过
updateChildren进行新旧节点的子节点的比对、更新
patchVnode 方法中主要做了如下操作:
- 如果
oldVnode===vnode, 节点没有变化,直接返回1
2
3if (oldVnode === vnode) {
return
} - 如果是
oldVnodevnode都为静态节点、key相同、vnode的isCloned为真或vnode.isOnce为真,则vnode直接从oldVnode中获取componentInstance, 然后返回1
2
3
4
5
6
7
8if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
} - 如果
vnode.text未定义:oldVnode.children和vnode.children存在且不相同,则调用updateChildren方法进行子节点的比对1
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
vnode.children存在,执行添加子节点操作1
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
oldVnode.children存在,执行删除子节点操作1
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
- 如果
oldVnode.text与vnode.text不相同,则更新为vnode.text1
2
3if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
updateChildren 方法内做了如下操作:
首先定义四个索引变量标记新旧子节点数组(ch、oldCh)的首尾,后面使用这些索引在子节点数组中移动进行子节点的比对
oldStartIdxnewStartIdx分别标记ch、oldCh的首节点oldEndIdxnewEndIdx分别标记ch、oldCh的尾节点
然后在一个循环中根据不同的情况对索引进行移动,当新子节点或旧子节点的首尾索引相遇时说明子节点已被比对完毕,此时应中止循环
1 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) |
循环中止后,有如下情况需要判断:
- 如果是
oldStartIdx>oldEndIdx导致循环中止,说明newStartIdx与newEndIdx之间的节点是新添加的,所以需要把它们添加到父节点中 - 如果是
newStartIdx>newEndIdx导致循环中止,说明oldStartIdx与oldEndIdx之间的节点被删除,所以需要从父节点中删除它们
接下来就是 updateChildren 进行子节点比对的核心逻辑了:

newStartNode与oldStartNode比对- 如果为
sameVnode,则调用patchVnode方法,最后oldStartIdxnewStartIdx向右移动
- 如果为
newEndNode与oldEndNode比对- 如果为
sameVnode,则调用patchVnode方法,最后oldEndIdxnewEndIdx向左移动
- 如果为
newEndNode与oldStartNode比对- 如果为
sameVnode,则调用patchVnode方法,然后把oldStartIdx标识的节点移动到oldEndIdx标识的节点的后面,最后oldStartIdx向右移动,newEndIdx向左移动
- 如果为
newStartNode与oldEndNode比对- 如果为
sameVnode,则调用patchVnode方法,然后把oldEndIdx标识的节点移动到oldStartIdx标识的节点的前面,最后newStartIdx向右移动,oldEndIdx向左移动
- 如果为
上面的条件都不成立
- 创建
oldCh中子节点的key与 子节点的在oldCh中的索引的映射,查询newStartIdx指向的vnode节点的key是否与映射中的key存在相同的情况,如果不存在则根据newStartIdx指向的vnode创建dom元素并插入到oldStartIdx指向的vnode节点的dom元素的前面 ; 如果存在且与映射中找到的节点为sameVnode, 则把找到的节点移动到oldStartIdx指向的节点的前面,并把找到的节点在映射中置为undefined,防止后面重复匹配,否则根据newStartIdx指向的vnode创建dom元素并插入到oldStartIdx指向的vnode节点的dom元素的前面