Math.min()
返回作为输入参数的数字中最小的一个,如果没有参数,则返回 Infinity。
什么时候需要 diff算法。
对于一个节点的操作主要有 3 种:挂载、更新和卸载。
在更新的时候,用到 diff 算法。
但不是所有的更新都需要用到 diff算法
文本更新不需要diff算法
什么场景下,我们需要 diff 算法呢?
对一组节点进行更新的时候
使用 diff算法 的目的,就是为了减少性能开销,提高效率!
const oldChildren = ul1.children;
const newChildren = ul2.children;
for (let i = 0; i < oldChildren.length; i++) {
// 调用 patch 函数依次更新子节点
patch(oldChildren[i], newChildren[i]);
}
diff算法需要处理的几种场景
两组元素个数相同的时候
只需要遍历两组子节点,依次更新每一个节点
const oldChildren = ul1.children;
const newChildren = ul2.children;
for (let i = 0; i < oldChildren.length; i++) {
// 调用 patch 函数依次更新子节点
patch(oldChildren[i], newChildren[i]);
}
两组元素个数不同的时候
在实际情况中,新的一组元素和旧的一组元素个数不一样
- 新的一组元素数量 > 旧的一组元素数量
-
- 新的一组元素数量 < 旧的一组元素数量 处理方法: 首先获取两组子节点公共的元素数量的长度, 然后如果新的一组元素多,则挂载剩余的新元素;新的一组元素少,则卸载旧元素即可
const oldChildren = ul1.children;
const newChildren = ul2.children;
const oldChildrenLength = oldChildren.length;
const newChildrenLength = newChildren.length;
// 获取公共的长度
const commonLength = Math.min(oldChildrenLength, newChildrenLength);
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 新的一组元素多,则挂载剩余的新元素
if (newChildrenLength > oldChildrenLength) {
for (let i = commonLength; i < newChildrenLength; i++) {
patch(null, newChildren[i]);
}
}
// 新的一组元素少,则卸载旧元素即可
else if (newChildrenLength < oldChildrenLength) {
for (let i = commonLength; i < oldChildrenLength; i++) {
unmount(oldChildren[i]);
}
}
两组元素的顺序不同
新的一组元素的顺序和旧的一组元素的顺序是不同的
//旧节点
[
{ type: "p", children: "我是p1" },
{ type: "p", children: "我是p2" },
{ type: "p", children: "我是p3" },
];
//新节点
[
{ type: "p", children: "我是p2" },
{ type: "p", children: "我是p3" },
{ type: "p", children: "我是p1" },
];
最高效的更新节点的方式是:将 我是p1 移动到新的一组节点的末尾即可。
引入新属性就是 key,来区分相同的 type 为 p 的节点
总结
diff算法 从理论的角度进行了分析,需要处理的 3 种不同的情况
vue3 中,它具体是如何实现这个 diff算法 的呢
diff算法 五大步
// vue-next/packages/runtime-core/src/renderer.ts/patchKeyedChildren 中
const patchKeyedChildren = (
oldChildren, // 旧的一组子节点
newChildren // 新的一组子节点
) => {
// 新的一组子节点的长度
const newChildrenLength = newChildren.length
// 旧的一组子节点中最大的 index
let oldChildrenEnd = oldChildren.length - 1
// 新的一组子节点中最大的 index
let newChildrenEnd = newChildrenLength - 1
// 1. 自前向后比对
let i = 0
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 {
...
}
}
第一步:自前向后比对
let i = 0;
while (i <= oldChildrenEnd && i <= newChildrenEnd) {
const oldVNode = oldChildren[i];
const newVNode = newChildren[i];
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode);
} else {
break;
}
i++;
}
//比较两个节点的 `type` 和 `key` 是否相同,如果相同,就认为是同一个节点
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key;
}
当 isSameVNodeType 返回 false 的时候,表示当key,type不相同时就会break
所以如果碰到一对新、旧节点,不是相同的节点(key,type不同时),循环就会提前 break 掉
第二步:自后向前比对
在第 1 步自前向后的循环结束之后,就会来到第 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--;
}
vue3 是如何求得最优的移动方案的呢