上文说了 render watch 回调(也就是组件初次挂载和更新)是调用 vm._update 方法,该方法内部调用vm.__patch__方法,可以找到该方法最终定义在 src/core/vdom/patch.js 中,核心源码如下:
// 两个判断函数
function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果没有新的vnode,并且有老的vnode,那直接调用老的vnode卸载方法
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
// 老的vnode不存在,就创建新的vnode
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 新老vnode都存在的情况
// 初始化渲染时oldVnode是真实的dom,才会有nodeType属性,所以为true
const isRealElement = isDef(oldVnode.nodeType);
// 如果不是初次渲染 并且新老节点是同一类型vnode,则进行后续对比
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 进行相同的vnode对比
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// 如果不是同一元素节点的话
if (isRealElement) {
// const SSR_ATTR = 'data-server-rendered'
// 如果是元素节点 并且有 'data-server-rendered' 这个属性
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
// 就是服务端渲染的,删掉这个属性
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
// 这个判断里是服务端渲染的处理逻辑,就是混合
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
// function emptyNodeAt (elm) {
// return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
// }
// 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
oldVnode = emptyNodeAt(oldVnode)
}
// 拿到 oldVnode 的父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
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)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
// 更新父节点
ancestor = ancestor.parent
}
}
// 如果旧节点还存在,就删掉旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 否则直接卸载 oldVnode
invokeDestroyHook(oldVnode)
}
}
}
// 返回更新后的节点
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
};
主要流程如下:
-
vnode 不存在,oldVnode 存在,就删掉 oldVnode
-
vnode 存在,oldVnode 不存在,就创建 vnode
-
两个都存在的话,通过 sameVnode 函数(tag 和 key 以及 input 类型是不是相同)对比是不是同一节点
- 相同的 vnode,通过 patchVnode 对比
- 不同的 vnode,就把老的替换成新的
patchVnode 方法:
function patchVnode (
oldVnode, // 老的虚拟 DOM 节点
vnode, // 新的虚拟 DOM 节点
insertedVnodeQueue, // 插入节点的队列
ownerArray, // 节点数组
index, // 当前节点的下标
removeOnly // 只有在
) {
// 新老节点引用地址是一样的,直接返回
// 比如 props 没有改变的时候,子组件就不做渲染,直接复用
if (oldVnode === vnode) return
// 新的 vnode 真实的 DOM 元素
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
// hook 相关的不用管
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取子元素列表
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events...
// 这里的 update 钩子函数是 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 这里的 update 钩子函数是我们传过来的函数
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果新节点不是文本节点,也就是说有子节点
if (isUndef(vnode.text)) {
// 如果新老节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果新节点有子节点的话,就是说老节点没有子节点
// 如果老节点文本节点,就是说没有子节点,就清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果新节点没有子节点,老节点有子节点,就删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老节点是文本节点,就清空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新老节点都是文本节点,且文本不一样,就更新文本
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
// 执行 postpatch 钩子
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
主要流程如下:
-
引用地址一样的,直接返回;
-
oldVnode 是异步组件,直接返回;
-
如果 oldVnode 和 vnode 都是静态节点,并且有一样的 key,直接赋值返回;
-
新节点不是文本节点也不是注释
- 新老节点都有子节点,并且子节点不一样,通过 updateChidlren 更新子节点;
- 只有新节点有子节点,则老节点新增子节点;
- 只有老节点有子节点,则老节点删除子节点;
- 如果老节点有文本则清空。
-
新节点是文本,和老节点不同则设置老节点的文本;
最后也就是 diff 算法的核心,updateChildren 方法:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 老 vnode 遍历的下标
let newStartIdx = 0 // 新 vnode 遍历的下标
let oldEndIdx = oldCh.length - 1 // 老 vnode 列表长度
let oldStartVnode = oldCh[0] // 老 vnode 列表第一个子元素
let oldEndVnode = oldCh[oldEndIdx] // 老 vnode 列表最后一个子元素
let newEndIdx = newCh.length - 1 // 新 vnode 列表长度
let newStartVnode = newCh[0] // 新 vnode 列表第一个子元素
let newEndVnode = newCh[newEndIdx] // 新 vnode 列表最后一个子元素
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
// 循环,规则是开始指针向右移动,结束指针向左移动移动
// 当开始和结束的指针重合的时候就结束循环
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)
// 然后把指针后移一位,从前往后依次对比
// 比如第一次对比两个列表的[0],然后比[1]...,后面同理
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)
// 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 新的 children 里有,可是没有在老的 children 里找到对应的元素
if (isUndef(idxInOld)) {
/// 就创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在老的 children 里找到了对应的元素
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 {
// 如果标签是不一样的,就创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// oldStartIdx > oldEndIdx 说明老的 vnode 先遍历完
if (oldStartIdx > oldEndIdx) {
// 就添加从 newStartIdx 到 newEndIdx 之间的节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
// 否则就说明新的 vnode 先遍历完
} else if (newStartIdx > newEndIdx) {
// 就删除掉老的 vnode 里没有遍历的节点
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
diff 算法使用了双指针,新老 vnode 列表分别定义两个指针,老的头部、老的尾部、新的头部、新的尾部,定义一个循环,结束条件就是新老任意一个列表的头部大于尾部,按优先级对比如下:
- 老的头部 对比 新的头部,判断 sameVnode,是则走上面的 patchVnode 方法,然后下标分别+1,继续对比;如果不同则走第 2 步;
- 老的尾部 对比 新的尾部,判断 sameVnode,是则走上面的 patchVnode 方法,然后下标分别-1,继续对比;如果不同则走第 3 步;
- 老的头部 对比 新的尾部,判断 sameVnode,是则走上面的 patchVnode 方法,移动老的 vnode,调整下标,继续对比;如果不同则走第 4 步;
- 老的尾部 对比 新的头部,判断 sameVnode,是则走上面的 patchVnode 方法,移动老的 vnode,调整下标,继续对比;如果不同则走第 5 步;
- 新的头部的 key 去老的列表中查找,找到则判断 sameVnode,是则走,是则走上面的 patchVnode 方法,移动老的 vnode,调整下标;没找到或者找到了但不是相同的 vnode,则将该新的 vnode copy 并移动到老的列表响应的位置;
头头对比可以理解,为什么尾尾、头尾、尾头对比呢?
尾尾:列表头部新插入一项,尾尾对比性能贼高;
头尾/尾头:主要是防止反转列表,再往头或尾插入数据;