Diff算法的目的
Diff算法的目的是为了找到哪些节点发生了变化,哪些节点没有发生变化可以复用。如果用最传统的
diff算法,如下图所示,每个节点都要遍历另⼀棵树上的所有节点做比较,这就是o(n^2)的复杂度,加
上更新节点时的o(n)复杂度,那就总共达到了o(n^3)的复杂度,这对于⼀个结构复杂节点数众多的页面,成本是非常大的。
所以实际上vue和react都对虚拟dom的diff算法做了优化了,将复杂度降低到了o(n)级别,具体的策略变为了:同层级的节点才能互相比较:
1. 节点比较时,如果类型不同,则对该节点及其所有子节点直接销毁新建;
2. 类型相同的字节点,使用key帮助查找,并且使用算法优化查找效率。其中react和vue2以及vue3
的diff算法都不尽相同;
前提:
• mount(vnode, parent, [refNode]) : 通过 vnode 生成真实的DOM节点。parent为其父
级的真实DOM节点, refNode 为真实的DOM节点,其父级节点为parent。如果refNode不为
空,vnode生成的DOM节点就会插⼊到refNode之前;如果refNode为空,那么vnode生成的DOM
节点就作为最后⼀个子节点插⼊到parent中
• patch(prevNode, nextNode, parent) : 可以简单的理解为给当前DOM节点进行更新,并
且调用diff算法对比自身的子节点;
双端比较
双端比较就是新列表和旧列表两个列表的头与尾互相对比,,在对比的过程中指针会逐渐向内靠拢,
直到某⼀个列表的节点全部遍历过,对比停止;(可以简单的理解对比头头,尾尾,头尾,尾头)
patch
- 先判断是否是首次渲染,如果是首次渲染那么我们就直接createElm即可;如果不是就去判断新老两个 节点的元素类型否⼀样;如果两个节点都是⼀样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明Vnode完全被改变了,就可以直接替换oldVnode;
patch源码如下:
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 判断新的vnode是否为空
if (isUndef(vnode)) {
// 如果⽼的vnode不为空 卸载所有的⽼vnode
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
// ⽤来存储 insert钩⼦函数,在插⼊节点之前调⽤
const insertedVnodeQueue = []
// 如果⽼节点不存在,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 是不是元素节点
const isRealElement = isDef(oldVnode.nodeType)
// 当⽼节点不是真实的DOM节点,并且新⽼节点的type和key相同,进⾏patchVnode更新⼯作
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 如果不是同⼀元素节点的话
// 当⽼节点是真实DOM节点的时候
if (isRealElement) {
// 如果是元素节点 并且在SSR环境的时候 修改SSR_ATTR属性
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
}
}
// 如果不是服务端渲染的,或者混合失败,就创建⼀个空的注释节点替换 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 的根节点存在,就是说根节点被修改了,就需要遍历更新⽗节点
// 递归 更新⽗占位符元素
// 就是执⾏⼀遍 ⽗节点的 destory 和 create 、insert 的 钩⼦函数
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)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
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)
}
}
}
// 执⾏ 虚拟 dom 的 insert 钩⼦函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm
}
patchVnode
• 如果 Vnode 和 oldVnode 指向同⼀个对象,则直接return即可;
• 将旧节点的真实 DOM 赋值到新节点(真实 dom 连线到新⼦节点)称为elm,然后遍历调⽤
update 更新 oldVnode 上的所有属性,⽐如 class,style,attrs,domProps,events...;
• 如果新⽼节点都有⽂本节点,并且⽂本不相同,那么就⽤ vnode .text更新⽂本内容。
• 如果oldVnode有⼦节点⽽ Vnode 没有,则直接删除⽼节点即可;
• 如果oldVnode没有⼦节点⽽ Vnode 有,则将Vnode的⼦节点真实化之后添加到DOM中即可。
• 如果两者都有⼦节点,则执⾏ updateChildren 函数⽐较⼦节点。
patchVnode的源码如下:
function patchVnode(
oldVnode, // 旧的虚拟 DOM 节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点队列
ownerArray, // 节点数组
index, // 当前节点的下标
removeOnly
) {
// 新旧节点的地址一样,直接跳过
if (oldVnode === vnode) {
return;
}
// 如果新节点有elm属性,并且ownerArray也有定义,说明是复用的节点,进行克隆
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = vnode.elm = oldVnode.elm;
// 如果当前节点是注释或v-if的,或者是异步函数,就跳过检查异步组件
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
// 执行hydrate,将新节点添加到旧节点中
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;
}
let i;
const data = vnode.data;
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
// 调用prepatch钩子函数
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用update更新oldVnode所有属性,比如class、style、attrs、domProps、eventListeners等
// 这⾥的 update 钩⼦函数是 vnode 本⾝的钩⼦函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
// 这⾥的 update 钩⼦函数是我们传过来的函数
// 调用用户自定义的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)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) {
// 调用postpatch钩子函数
i(oldVnode, vnode);
}
}
}
代码解释:
这段代码是用来更新虚拟DOM节点的,它会对比新旧节点的属性和子节点,并进行相应的更新操作
- 首先,它检查新旧节点的引用是否相同,如果相同,则直接返回,无需进行任何更新操作。
- 如果新节点具有elm属性并且ownerArray也被定义,说明节点是被复用的,它会克隆新节点。
- 将新节点的elm属性设置为旧节点的elm属性,以保持引用的一致性。
- 如果旧节点是异步占位符节点(例如v-if指令的节点),则根据新节点是否已经解析,决定是执行hydrate操作(将新节点添加到旧节点中)还是将新节点标记为异步占位符。
- 如果新节点和旧节点都是静态节点,并且它们具有相同的key,并且满足以下条件之一:新节点被克隆或者具有v-once指令,那么只需将旧节点的组件实例赋值给新节点的组件实例,并返回。
- 如果新节点具有data属性,并且该节点可进行补丁操作(isPatchable函数返回true),则遍历调用cbs.update数组中的各个函数来更新旧节点的属性。如果data.hook.update钩子函数被定义,则也会调用它。
- 如果新节点不是文本节点(即具有子节点):
-
- 如果旧节点和新节点都具有子节点,并且它们不相等,则调用updateChildren函数来对比和更新子节点。
- 如果只有新节点具有子节点,而旧节点没有子节点,则清空旧节点的文本内容,并通过addVnodes函数添加新的子节点。
- 如果只有旧节点具有子节点,而新节点没有子节点,则通过removeVnodes函数删除旧节点的子节点。
- 如果旧节点是文本节点,则清空旧节点的文本内容。
- 如果新节点是文本节点,并且新节点的文本内容与旧节点的文本内容不同,则更新旧节点的文本内容为新节点的文本内容。
- 如果新节点的data属性被定义,且存在data.hook.postpatch钩子函数,则调用它
核心部分updateChildren()
手动实现vue2的updateChildren:
实现思路
我们首先先定义四个指针(下标)指向两个列表的头尾
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[nextStartIndex],
newEndNode = nextChildren[nextEndIndex];
}
根据四个指针找到四个节点,然后进行对比,那么如何对比呢?我们按照以下四个步骤进行对比
概括:头头对比/尾尾对比/头尾对比/尾头对比
- 使用旧列表的头⼀个节点 oldStartNode 与新列表的头⼀个节点 newStartNode 对比;
- 使用旧列表的最后⼀个节点 oldEndNode 与新列表的最后⼀个节点 newEndNode 对比;
- 使用旧列表的头⼀个节点 oldStartNode 与新列表的最后⼀个节点 newEndNode 对比;
- 使用旧列表的最后⼀个节点 oldEndNode 与新列表的头⼀个节点 newStartNode 对比;
使用以上四步进行对比,去寻找key相同的可复用的节点,当在某⼀步中找到了则停止后面的寻找。具
体对比顺序如下图:
对比顺序代码结构如下:
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[nextStartIndex],
newEndNode = nextChildren[nextEndIndex];
if (oldStartNode.key === newStartNode.key) {
} else if (oldEndNode.key === newEndNode.key) {
} else if (oldStartNode.key === newEndNode.key) {
} else if (oldEndNode.key === newStartNode.key) {
}
}
如以上代码当对比时找到了可复用的节点,我们还是先 patch 给元素打补丁,然后将指针进行前/后移⼀位指针。根据对比节点的不同,我们移动的指针和方向也不同,具体规则如下:
1. 当旧列表的头⼀个节点 oldStartNode 与新列表的头⼀个节点 newStartNode 对比时key相
同。那么旧列表的头指针 oldStartIndex 与新列表的头指针 newStartIndex 同时向后移动⼀位
2. 当旧列表的最后⼀个节点 oldEndNode 与新列表的最后⼀个节点 newEndNode 对比时key相
同。那么旧列表的尾指针oldEndIndex与新列表的尾指针 newEndIndex 同时向前移动⼀位;
3. 当旧列表的头⼀个节点 oldStartNode 与新列表的最后⼀个节点 newEndNode 对比时key相
同。那么旧列表的头指针 oldStartIndex 向后移动⼀位;新列表的尾指针 newEndIndex 向
前移动⼀位;
4. 当旧列表的最后⼀个节点 oldEndNode 与新列表的头⼀个节点 newStartNode 对比时key相
同。那么旧列表的尾指针 oldEndIndex 向前移动⼀位;新列表的头指针 newStartIndex 向
后移动⼀位;
编写成代码如下可视:
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[nextStartIndex],
newEndNode = nextChildren[nextEndIndex];
if (oldStartNode.key === newStartNode.key) {
patch(oldvStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
oldEndIndex--
nextStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
上面提到,要让指针向内靠拢,所以我们需要循环。循环停止的条件是当其中⼀个列表的节点全部遍
历完成(即老的头下标小于等于老的尾下标并且新的头下标小于新的尾下标),代码如下:
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[nextStartIndex],
newEndNode = nextChildren[nextEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
patch(oldvStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
oldEndIndex--
nextStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
至此整体的循环我们就全部完成了,下⾯我们需要考虑这样两个问题:
• 什么情况下DOM节点需要移动;
• DOM节点如何移动;
什么情况下DOM节点需要移动
我们以上面的图为例子
理想状态的情况
当我们在第⼀个循环时,在第四步发现旧列表的尾节点 oldEndNode 与新列表的头节点 newStartNode 的key相同,是可复用的DOM节点。通过观察我们可以发现,原本在旧列表末尾的节点,却是新列表中的开头节点,没有人比他更靠前,因为他是第⼀个,所以我们只需要把当前的节 点移动到原本旧列表中的第⼀个节点之前,让它成为第一个节点即可。代码如下
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
// ...
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
// 移动到旧列表头节点之前
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
}
进⼊第⼆次循环,我们在第⼆步发现,旧列表的尾节点 oldEndNode 和新列表的尾节点 newEndNode 为复用节点。原本在旧列表中就是尾节点,在新列表中也是尾节点,说明该节点不需要移动,所以我们什么都不需要做。
同理,如果是旧列表的头节点 oldStartNode 和新列表的头节点 newStartNode 为复用节点,我
们也什么都不需要做
进⼊第三次循环,我们在第三部发现,旧列表的头节点 oldStartNode 和新列表的尾节点
newEndNode 为复⽤节点。,我们只要将DOM-A移动到DOM-B后⾯就可以了。
依照惯例我们还是解释⼀下,原本旧列表中是头节点,然后在新列表中是尾节点。那么只要在旧列表
中把当前的节点移动到原本尾节点的后⾯,就可以了。
代码如下:
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
//...
}
}
}
进⼊最后⼀个循环。在第⼀步旧列表头节点 oldStartNode 与新列表头节点 newStartNode 位置
相同,所以啥也不⽤做。然后结束循环。
非理想情况
上⽂中有⼀个特殊情况,当四次对比都没找到复用节点时,我们只能拿新列表的第⼀个节点去旧列表
中找与其key相同的节点。如下图所示
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
}
}
}
拿新列表的节点去旧列表中找只有两种情况:
- 一种是在旧列表找到了节点--此时应该移动节点
- 一种在就列表找不到对应的节点--此时应该新增节点
第一种情况如图所示
当我们在旧列表中找到对应的VNode,我们只需要将找到的节点的DOM元素,移动到开头就可以了。
这里的逻辑其实和第四步的逻辑是⼀样的,只不过第四步是移动的尾节点,这里是移动找到的节点。
DOM移动后,由我们将旧列表中的节点改为undefined,这是至关重要的⼀步,因为我们已经做了节
点的移动了所以我们不需要进行再次的对比了。最后我们将头指针newStartIndex向后移⼀位
代码如下
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
if (oldIndex > -1) {
let oldNode = prevChildren[oldIndex];
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
第二种情况如果在旧列表中没有找到复⽤节点,就直接创建⼀个新的节点放到最前⾯就可以了,然后后移头指针 newStartIndex 。如图所示:
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
if (oldIndex > -1) {
let oldNode = prevChildren[oldIndex];
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
} else {
mount(newStartNode, parent, oldStartNode.el)
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
我们上面说到若是找到相同的把节点改为undefined的,此时我们遍历需要考虑这种情况,所以当旧列表遍历到undefind时就跳过当前节点。代码如下:
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//旧节点才比较
//若是头部找到undefine则往后移动一位
if (oldStartNode === undefind) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefind) {
//若是尾部找到undefine则往前移动一位
oldEndNode = prevChildren[--oldEndIndex]
} else if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// ...
}
}
}
添加节点的情况
如图所示:
此时 oldEndIndex 以及小于了 oldStartIndex ,但是新列表中还有剩余的节点,我们只需要将剩余的节点依次插入到 oldStartNode 的DOM之前就可以了。
为什么是插入 oldStartNode 之前呢?原因是剩余的节点在新列表的位置是位于 oldStartNode 之前的,如果剩余节点是在 oldStartNode 之后,oldStartNode 就会先行对比
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// ...
}
if (oldEndIndex < oldStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
}
}
移除节点的情况
当新列表的 newEndIndex 小于 newStartIndex 时,我们将旧列表剩余的节点删除即可。这里我
们需要注意,旧列表的undefind。在第二小节中我们提到过,当头尾节点都不相同时,我们会去旧列
表中找新列表的第⼀个节点,移动完DOM节点后,将旧列表的那个节点改为undefind。所以我们在最
后的删除时,需要注意这些undefind,遇到的话跳过当前循环即可。
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// ...
}
if (oldEndIndex < oldStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
} else if (newEndIndex < newStartIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (prevChildren[i]) {
partent.removeChild(prevChildren[i].el)
}
}
}
}
总结
最终手写实现updateChildren代码如下:
function vue2diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
newStartIndex = 0,
oldStartIndex = prevChildren.length - 1,
newStartIndex = nextChildren.length - 1,
oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldStartIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newStartIndex];
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
if (oldStartNode === undefined) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefined) {
oldEndNode = prevChildren[--oldStartIndex]
} else if (oldStartNode.key === newStartNode.key) {
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldStartIndex--
newStartIndex--
oldEndNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newStartIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldStartIndex--
newStartIndex++
oldEndNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else {
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child && (child.key === newKe
if (oldIndex === -1) {
mount(newStartNode, parent, oldStartNode.el)
} else {
let prevNode = prevChildren[oldIndex]
patch(prevNode, newStartNode, parent)
parent.insertBefore(prevNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartIndex++
newStartNode = nextChildren[newStartIndex]
}
}
if (newStartIndex > newStartIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
parent.removeChild(prevChildren[oldStartIndex++].el)
}
} else if (oldStartIndex > oldStartIndex) {
while (newStartIndex <= newStartIndex) {
mount(nextChildren[newStartIndex++], parent, oldStartNode.el)
}
}
}
缺点
Vue2 是全量 Diff(当数据发生变化,它就会新生成⼀个DOM树,并和之前的DOM树进行比较,找到不
同的节点然后更新),如果层级很深,很消耗内存;所以Vue3因此做出的改变