四个方法
sameVnode
sameVnode方法用于比较两个节点是否是同类型的节点(key、tag等属性相同),决定是否复用节点
function sameVnode(a, b) {
// Tips: 判断条件删除了部分判断条件
return (
a.key === b.key && // key是否相同
a.tag === b.tag && // tag是否相同
a.isComment === b.isComment && // 是否注释节点
isDef(a.data) === isDef(b.data) && // data是否都已定义或者未定义
sameInputType(a, b) // 节点为input时,判断input.type是否相同
)
}
patch
patch方法是diff流程的入口。会简单的判断下有无新节点与旧节点:
- 没有新节点:结束patch方法的执行
- 没有旧节点,但有新节点:不需比较,直接创建对应的新元素
- 有旧节点,且有新节点:调用sameVnode方法判断节点是否可复用
- 可复用节点:调用patchVnode方法继续比较新、旧节点
- 不可复用节点:直接创建对应的新元素,并移除旧节点
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// Cond: 没有新节点,结束patch方法的执行
if (isDef(oldVnode)) {
invokeDestroyHook(oldVnode)
}
return
}
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// Cond: 没有旧节点,只有新节点
// 直接创建对应元素
createElm(vnode, insertedVnodeQueue)
} else {
// Cond: 有旧节点,有新节点
// 判断节点是否可复用
if (sameVnode(oldVnode, vnode)) {
// Cond: 可复用节点
// 执行patchVnode比较新旧节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// Cond: 不可复用节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 创建新元素
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 移除旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
return vnode.elm
}
patchVnode
patchVnode方法用于比较新节点与旧节点。会比较新节点与旧节点:
- 新节点非文本节点:判断新节点、旧节点是否有子节点
- 有新子节点,且有旧子节点:调用updateChildren方法比较新、旧子节点
- 有新子节点,但没有旧子节点:直接添加新子节点
- 没有新子节点,但有旧子节点:直接移除旧子节点
- 没有新子节点,且没有旧子节点,且旧节点是文本节点:清空文本内容
- 新节点是文本节点,且新节点的文本内容与旧节点不同:更新文本内容
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
if (oldVnode === vnode) {
// Cond: 旧节点与新节点相等
// 直接结束patchVnode的执行
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 克隆复用节点
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = (vnode.elm = oldVnode.elm)
// diff优化:对于克隆的静态节点直接复用其元素,并结束patchVnode的执行
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
const oldCh = oldVnode.children
const ch = vnode.childrenMatch
if (isUndef(vnode.text)) {
// Cond: 非文本节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) {
// Cond: 有旧子节点数组,且有新子节点数组,且二者不相等
// 调用updateChildren方法比较子节点数组
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
} else if (isDef(ch)) {
// Cond: 有新子节点数组,且没有旧子节点数组
if (isDef(oldVnode.text)) {
// Cond: 旧节点是文本节点
// 清空文本内容
nodeOps.setTextContent(elm, '')
}
// 添加新子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// Cond: 有旧子节点数组,且没有新子节点数组
// 移除旧子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// Cond: 旧节点是文本节点
// 清空文本内容
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// Cond: 文本节点且文本内容不同
// 更新文本内容
nodeOps.setTextContent(elm, vnode.text)
}
}
updateChildren
updateChildren方法用于比较新、旧子节点,会从两端向中间对比新、旧子节点数组,因此需要先了解几个自定义的子节点名称:
- 旧前节点:旧子节点数组左侧的节点
- 旧后节点:旧子节点数组右侧的节点
- 新前节点:新子节点数组左侧的节点
- 新后节点:新子节点数组右侧的节点
第一步
依次判断如下条件,满足条件时执行对应条件分支语句,然后开始下一次循环:
- 没有旧前节点:获取下一个旧前节点
- 没有旧后节点:获取下一个旧后节点
- 新前节点可复用旧前节点:调用patchVnode方法,获取下一个新前、旧前节点
- 新后节点可复用旧后节点:调用patchVnode方法,获取下一个新后、旧后节点
- 新后节点可复用旧前节点:调用patchVnode方法,将旧前节点对应的元素移到旧后节点前,获取下一个新后、旧前节点
- 新前节点可复用旧后节点:调用patchVnode方法,将旧后节点对应的元素移到旧前节点前,获取下一个新前、旧后节点
- 以上条件都不满足:根据新前节点的key判断旧子节点中是否有与之对应的子节点,然后获取下一个新前节点
- 没有对应的子节点:直接创建新的元素
- 有对应的子节点:调用sameVnode,判断节点是否可复用
- 可复用:调用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
// 开始循环遍历新、旧子节点数组
// 需要清晰的是:
// 当新、旧子节点数组其一遍历完成,则结束while循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// Cond: 无旧前子节点
// 获取下一个旧前子节点
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
// Cond: 无旧后子节点
// 获取下一个旧后子节点
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// Cond: 新前子节点可复用旧前节点
// 调用patchVnode比较新子节点与旧子节点
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 获取下一个旧前、新前子节点
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// Cond: 新后子节点可复用旧后子节点
// 调用patchVnode比较新子节点与旧子节点
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 获取下一个旧前、新后子节点
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Cond: 新后子节点可复用旧前子节点
// 调用patchVnode比较新子节点与旧子节点
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
// 将旧前节点对应的元素移动到右侧(旧后节点对应的元素之前)
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
// 获取下一个旧前、新后子节点
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Cond: 新前子节点可复用旧后子节点
// 调用patchVnode比较新子节点与旧子节点
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 将旧后节点对应的元素移动到左侧(旧前节点对应的元素之前)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 获取下一个旧后、新前子节点
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// Cond: 以上条件都不满足
// 获取未遍历的旧子节点数组中的节点的key与索引对应的关系对象:{ [key]: [index] }
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 首先会判断旧子节点数组中是否有与新前节点key相同的节点
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// Cond: 没有在旧子节点数组找到与新前节点对应的节点
// 直接创建新元素
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
// Cond: 有新前节点对应的旧节点
// 判断节点是否可复用
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// Cond: 可复用
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldCh[idxInOld] = undefined
// 将复用的节点移动到左侧(旧前节点之前)
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// Cond: 不可复用
// 直接创建新元素
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)
}
}