diff算法

75 阅读4分钟

Math.min()

返回作为输入参数的数字中最小的一个,如果没有参数,则返回 Infinity

什么时候需要 diff算法

对于一个节点的操作主要有 3 种:挂载、更新和卸载。

在更新的时候,用到 diff 算法。

但不是所有的更新都需要用到 diff算法 文本更新不需要diff算法

image.png

什么场景下,我们需要 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]);
}

两组元素个数不同的时候

在实际情况中,新的一组元素和旧的一组元素个数不一样

  1. 新的一组元素数量 > 旧的一组元素数量
    1. 新的一组元素数量 < 旧的一组元素数量 处理方法: 首先获取两组子节点公共的元素数量的长度, 然后如果新的一组元素多,则挂载剩余的新元素;新的一组元素少,则卸载旧元素即可
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 是如何求得最优的移动方案的呢