提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
一、diff算法是什么?
diff算法很早就存在了,一开始diff算法是用来计算出两个文本的差异。所以大家一定要明确,diff算法并不是react或者vue原创的,它们只是用diff算法来比较两个vnode的差异,并且只针对该部分进行原生DOM操作,而非重新渲染整个页面。而在vue里,在更新虚拟DOM的在patch(vnode, newVnode)方法中,比较新旧函数时会用到diff。
二、vue2中的diff算法
vue2中使用的diff算法是双端比较,以下是vue2中实现diff的主要步骤。
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = preChildren.length - 1,
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 头头 尾尾 头尾 尾头
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);
oldEndIndex--;
newEndIndex--;
oldEndNode = prevChildren[oldEndIndex];
newEndNode = nextChildren[newEndIndex];
} 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) {
patch(oldEndNode, newStartNode, parent);
// 把老的最后一个节点挪到最前面
parent.insertBefore(oldEndNode.el, oldStartNode.el);
oldEndIndex--;
newStartIndex++;
oldEndNode = prevChildren[oldEndIndex];
newStartNode = nextChildren[newStartIndex];
} else {
// 四次比较都没有比较到
// 拿着新的newStartNode中的当前的key,遍历prevChildren,找有没有相同的key
let newkey = newStartNode.key,
oldIndex = prevChildren.findIndex(
(child) => child.key === newKey
);
if (oldIndex > -1) {
// 匹配到了
let oldNode = prevChildren[oldEndIndex];
patch(oldNode, newStartNode, parent);
parent.insertBefore(oldNode.el, oldStartNode.el);
prevChildren[oldEndIndex] = undefined; // 当前序号为oldIndex中置为空
} else {
// 没匹配到,直接创建一个新的节点放在开头就好了
mount(newStartNode.el, parent, oldStartNode.el);
}
newStartNode = nextChildren[++newStartIndex];
}
}
if (oldEndIndex < oldStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i]);
}
} else if (newEndIndex < newStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
parent.remove(prevChildren[i]);
}
}
}
双端比较的流程:
- 头头比较:首先是老的节点数组的头结点和新节点数组的头节点进行比较,如果相同(说明当前节点为发生变化,无需修改虚拟DOM),则老的节点和新的节点同时向后移动一位,再进行比较;如果不相同,则启动尾尾比较。
- 尾尾比较:老的节点数组的尾结点和新节点数组的尾节点进行比较,如果相同,则两者都向前移动一位,之后老的节点数组右移,新节点数组左移,再进行比较;如果不相同,则进行头节点和尾节点比较。
- 头尾比较:老的节点数组的头节点和新的节点数组的尾节点比较,如果相同,说明老的节点在新节点数组中被移动到最后一位了,就把老的头节点放到最后,再进行比较;如果如果不相同,则进行尾节点和头节点比较。
- 尾头比较:新的节点数组的头节点和老的节点数组的尾节点比较,如果相同,说明老的节点在新节点数组中被移动到第一位了,就把老的头节点放到最开头,之后老的节点数组左移,新节点数组右移,再进行比较;如果前四步都走完还不匹配,则进入else的兜底判断。
- 拿着新的新节点的当前的key,遍历老节点数组,找有没有相同的key,如果有,则把老数组中的对应节点放到新的数组中对应key值的Index处,如果没匹配到,就直接创建新节点。
三、vue3中的diff算法
vue3中的diff算法相比起vue2的双端比较进行了升级,通过求得最长递增子序列(此处用到了贪婪算法和二分查找优化效率),使得diff效率更高。
// vue-next/packages/runtime-core/src/renderer.ts/patchKeyedChildren 中
const patchKeyedChildren = (
oldChildren, // 旧的一组子节点
newChildren, // 新的一组子节点
) => {
let i = 0
// 新的一组子节点的长度
const newChildrenLength = newChildren.length
// 旧的一组子节点中最大的 index
let oldChildrenEnd = oldChildren.length - 1
// 新的一组子节点中最大的 index
let newChildrenEnd = newChildrenLength - 1
// 1. 自前向后比对
while (i <= oldChildrenEnd && i <= newChildrenEnd) {
const oldVNode = oldChildren[i];
const newVNode = newChildren[i];
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode);
} else {
break;
}
i++;
}
// 2. 自后向前比对
// 旧的一组子节点中最大的 index
let oldChildrenEnd = oldChildren.length - 1;
// 新的一组子节点中最大的 index
let newChildrenEnd = newChildrenLength - 1;
while (i <= oldChildrenEnd && i <= newChildrenEnd) {
const oldVNode = oldChildren[oldChildrenEnd];
const newVNode = newChildren[newChildrenEnd];
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode, container, null);
} else {
break;
}
oldChildrenEnd--;
newChildrenEnd--;
}
// 3. 新节点多于旧节点,挂载多的新节点
if (i > e1) {
if (i <= e2) {
...
}
}
// 4. 新节点少于旧节点,卸载多的旧节点
else if (i > e2) {
while (i <= e1) {
...
}
}
// 5. 乱序
else {
...
}
}
vue3中的diff算法大致分为五步
- 头序比较算法,老节点数组和新节点数组的头结点相互比较,如果相同,则同时后移,直到比较到发现不同的情况为止,此时启动尾序比较算法。
- 尾序比较算法,即老节点数组和新节点数组的尾结点相互比较,如果相同,则同时前移,也是直到比较到发现不同的情况为止。
- 判断,此时如果新节点数组中有节点但老节点数组中没有节点,则创建一个新节点插入对应位置。
- 如果老节点数组中存在的节点在新的节点数组中不存在,则卸载掉老的节点。
- 上述四步结束过后就会剩下一些乱序节点,即节点既存在老的节点数组,又存在于新的节点数组,只不过节点所在的位置发生了变化,因此只需要找到老节点在新节点数组中的位置并把它移动过去就可以了,也就是在这里,vue3使用了最长递增子序列,先固定一个最长的不需要移动的数组,在把不在这个最长递增子序列中的节点外的乱序节点插入其中,以实现最小的移动消耗就能实现最终的效果。至于vue3中实现最长递增子序列的过程可以参考我之前的: vue3源码中的最长递增子序列的实现方式,这里就不再赘述了。
总结
本文简单介绍了vue2和vue3中diff算法的大致实现流程,具体更详尽的代码建议看vue的源码,里面还有很多细节。