一直不想写 vue diff 的过程,因为 这一块网络上讲的人实在是太多了,这里写了也只不过是拾人牙慧罢了,但是不写吧,又觉得心痒痒的,毕竟前面写了一篇 react 源码中 对 key 的使用 的
首先是几个工具函数 有关于更新节点的内容,看一下我的 vue 中 patch、patchVnode 函数(更新节点)
// 这里用来判断 一个值是否是 未定义的
export function isDef(v: any): boolean % checks {
return v !== undefined && v !== null
}
// 对比 是不是 新节点
// 这里和之前的区别是, 没有设置 key 的情况下,key 被认为是相等的
function findIdxInOld(node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
// 两个 vnode 是否相同
function sameVnode(a, b) {
return (
// 需要注意的是 null === null 为 true
// 也就是说,如果 没有设置 key 的话,那么就默认为 两个节点的key 是一致的
// 对比于 react 的diff ,这里的判断 多出了 tag、comment 和 input 的判断
// 而 react 则着重于 $$type 的判断
// 这里 react 明显的 对于 jsx 的分类 更倾向于 自己的判断,而 vue 则是 倾向于 用户的输入
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
// 是否是 同一个类型的 input 标签,这个很好理解
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
// 是否是 相同类型的 input 节点,比如 radio,text,checkbox等
function sameInputType(a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
真正 diff 的过程
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)
}
// 而这里,也是和 react diff 的最大的不同之处了,
// react 对于新旧节点只做了顺序的 查找 相同节点,而 vue 还做了 前后节点的 对比
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 {
// 这里是 为 老节点的 key 和 位置 做一个 对应关系
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 查找 在 老节点的 key 对应 map 中有没有 可以复用的节点
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]
// 然后再对比 input 的type 和 tag 等属性,一样的话,吧老节点 对应的内容制空,复用节点
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 虽然 key 相等,但是节点类型不同,创建新节点
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)
}
}
- 获取老节点的排头和排尾,以及新节点的排头和排尾
- 遍历一直到老节点排头和排尾都为 存在的值结束
- 当 老节点的排头和新节点的排头是相同节点的时候 进入 patchVnode ,开始进一步比较这两个节点,同时 新老节点的排头和排尾往后移动一位
- 老节点的排尾和新节点的排尾是相同节点 的时候,进入 patchVnode,同时新老节点的排尾 向前移动一位
- 当 老节点的排头和新节点的排尾 是相同节点 的时候,进入 patchVnode 深度比较,然后对 节点在 dom 上的位置进行移动,接着老节点的排头往后移动,新节点的排尾往前移动、
- 当 老节点的排尾和新节点的排头是相同节点 的时候,进入 patchVnode 深入比较,然后对 对应的节点在 dom 上的位置进行移动,然后 老节点的排尾向前移动,新节点的排头向后移动
- 当 前面的 3、4、5、6 都不符合之后,将 老节点的 key 和对应的index 作为一个 map 对象保存起来
- 然后将新节点剩下的节点 对应的 key 寻找老节点对应相同的 key
- 如果存在相同的节点,将对应的节点 进入 patchVnode,然后将老节点 key 对应的value 职位 undefined,也就是删除,接着将老节点插入
- 如果最后遍历结束,新节点还有剩余,那创建新节点,如果老节点还有剩余,那删除老节点
所以 react 和 vue 的 diff 之间的异同?
当 key 为 null,也就是没有设置 key 的时候,两个都是当作 相同的节点来进行的
react 和 vue 中对应 两个节点是否 可以复用 有着不同的判断
react 中的 复用,指的是 key 相等,然后 type 相等,这里的 type 是在创建的时候,根据节点类型 设置的
而 vue 的 复用,指的是 key 相等,节点的 tag 类型相等,input 的 type 相等
而 vue diff 中的 优化,指的就是 对 排头排尾 的比较了,