简单diff算法
土豆 2023/7/14 vue
# 减少 DOM 操作的性能开销
1.默认处理
旧的全部卸载,然后再将新的一组子节点全部挂载到容器中
// 代码运行到这里,则说明旧子节点都是一组节点,这里涉及及核心的Diff算法
// 将旧的一组子节点全部卸载
n1.children.forEach((c) => unmount(c));
// 再将新的一组子节点全部挂载到容器中
n2.children.forEach((c) => patch(null, c, container));
2.减少操作
- 取两个数组的最小长度,最小长度部分新的节点替换旧的节点
- 如果新数组的长度大于旧数组长度说明有新增,直接挂载到后面
- 如果新数组小于旧的数组长度说明有删除,把多余最小长度的部分给卸载掉
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 旧的一组子节点的长度
const oldLen = oldChildren.length;
// 新的一组子节点的长度
const newLen = newChildren.length;
// 两组子节点的公共长度,即两者中较短的那一组子节点的长度
const commonLength = Math.min(oldLen, newLen);
// 遍历commonLength 次
// 遍历children
for (let i = 0; i < commonLength; i++) {
// 调用patch 函数逐个更新子节点
patch(oldChildren[i], newChildren[i], container);
}
// 如果 newLen > oldLen,说明有新子节点需要挂载
if (newLen > oldLen) {
for (let i = commonLength; i < newLen; i++) {
patch(null, newChildren[i], container);
}
} else if (oldLen > newLen) {
// 如果oldLen > newLen, 说明有旧的子节点需要卸载
for (let i = commonLength; i < oldLen; i++) {
unmount(oldChildren[i]);
}
}
# DOM 复用与 key 的作用
const oldVnode = {
type: "div",
children: [
{ type: "p", children: "1", key: 1 },
{ type: "p", children: "2", key: 2 },
{ type: "p", children: "hello", key: 3 },
],
};
const newVnode = {
type: "div",
children: [
{ type: "p", children: "world", key: 3 },
{ type: "p", children: "1", key: 1 },
{ type: "p", children: "2", key: 2 },
],
};
key 处理
遍历新的数组和旧数组中的 key 进行比较,如果 key 相同则更新
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
const newVnode = newChildren[i];
let j = 0;
// 遍历旧的children
for (j; j < oldChildren.length; j++) {
const oldVnode = oldChildren[j];
// 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode, container);
break;
}
}
}
修改值后
const vnode = {
type: "div",
children: [
{ type: "p", children: "1", key: 1 },
{ type: "p", children: "2", key: 2 },
{ type: "p", children: "world", key: 3 },
],
};
修改后虚拟 DOM 顺序不正确,需要找到要移动的元素
# 找到需要移动的元素
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;
// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
const newVnode = newChildren[i];
// 遍历旧的children
for (let j = 0; j < oldChildren.length; j++) {
const oldVnode = oldChildren[j];
// 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode, container);
if (j < lastIndex) {
// 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
// 说明该节点对应的真实 DOM 需要移动
} else {
// 如果当前找到的节点在旧 children 中的索引不小于最大索引值,
// 则更新 lastIndex 的值
lastIndex = j;
}
console.log(lastIndex);
break;
}
}
}
# 移动元素
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;
// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
const newVnode = newChildren[i];
// 遍历旧的children
for (let j = 0; j < oldChildren.length; j++) {
const oldVnode = oldChildren[j];
// 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
if (newVnode.key === oldVnode.key) {
patch(oldVnode, newVnode, container);
if (j < lastIndex) {
// 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
// 说明该节点对应的真实 DOM 需要移动
const preVNode = newChildren[i - 1];
if (preVNode) {
// 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
const anchor = preVNode.el.nextSibling;
// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
// 也就是 prevVNode 对应真实 DOM 的后面
insert(newVnode.el, container, anchor);
}
} else {
// 如果当前找到的节点在旧 children 中的索引不小于最大索引值,
// 则更新 lastIndex 的值
lastIndex = j;
}
break;
}
}
}
# 添加新元素
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;
// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
const newVnode = newChildren[i];
// 遍历旧的children
// 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
// 初始值为 false,代表没找到
let find = false;
for (let j = 0; j < oldChildren.length; j++) {
const oldVnode = oldChildren[j];
// 如果找到了具有相同key值的两个节点,则调用`patch` 函数更新之
if (newVnode.key === oldVnode.key) {
// 一旦找到可复用的节点,则将变量 find 的值设为 true
find = true;
patch(oldVnode, newVnode, container);
if (j < lastIndex) {
// 如果当前找到的节点在旧 children 中的索引小于最大索引值lastIndex,
// 说明该节点对应的真实 DOM 需要移动
const preVNode = newChildren[i - 1];
if (preVNode) {
// 由于我们要将 newVNode 对应的真实 DOM 移动到 prevVNode 所对应真实 DOM 后面,
// 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
const anchor = preVNode.el.nextSibling;
// 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
// 也就是 prevVNode 对应真实 DOM 的后面
insert(newVnode.el, container, anchor);
}
} else {
// 如果当前找到的节点在旧 children 中的索引不小于最大索引值,
// 则更新 lastIndex 的值
lastIndex = j;
}
break;
}
}
// 如果代码运行到这里,find 仍然为 false,
// 说明当前 newVNode 没有在旧的一组子节点中找到可复用的节点
// 也就是说,当前 newVNode 是新增节点,需要挂载
if (!find) {
// 为了将节点挂载到正确位置,我们需要先获取锚点元素
// 首先获取当前 newVNode 的前一个 vnode 节点
const prevVNode = newChildren[i - 1];
let anchor = null;
if (prevVNode) {
// 如果有前一个 vnode 节点,则使用它的下一个兄弟节点作为锚点元素
anchor = prevVNode.el.nextSibling;
} else {
// 如果没有前一个 vnode 节点,说明即将挂载的新节点是第一个子节
// 这时我们使用容器元素的 firstChild 作为锚点
anchor = container.firstChild;
}
// 挂载 newVNode
patch(null, newVnode, container, anchor);
}
}
# 移除不存在的元素
// 新旧children
const oldChildren = n1.children;
const newChildren = n2.children;
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0;
// 遍历新的children
for (let i = 0; i < newChildren.length; i++) {
const newVnode = newChildren[i];
// 遍历旧的children
// 在第一层循环中定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,
// 初始值为 false,代表没找到
let find = false;
for (let j = 0; j < oldChildren.length; j++) {
// 省略...
}
if (!find) {
// 省略...
}
// 遍历旧的节点
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i];
// 拿旧子节点 oldVNode 去新的一组子节点中寻找具有相同 key 值的节点
const has = newChildren.find((vnode) => vnode.key === oldVNode.key);
if (!has) {
// 如果没有找到相同的节点,则移除
unmount(oldVNode);
}
}
}