Vue3、 Vue2 Diff算法比较

Vue2 Diff算法

源码位置:src/core/vdom/patch.ts

源码所在函数:updateChildren()

源码讲解:

  • 有新旧两个节点数组:oldChnewCh

  • 有下面几个变量:

    oldStartIdx 初始值=0

    oldStartVnode 初始值=oldCh[0]

    oldEndIdx 初始值=oldCh.length - 1

    oldEndVnode 初始值=oldCh[oldEndIdx]

    newStartIdx 初始值=0

    newStartVnode 初始值=newCh[0]

    newEndIdx 初始值=newCh.length - 1

    newEndVnode. 初始值=newCh[newEndIdx]

  • 对比流程

  1. 新旧数组,从首到尾对比,直到Vnode不相同

图片

 2. 新旧数组,从尾到首对比,直到Vnode不相同

图片

 3. 旧数组尾和新数组首对比,直到Vnode不同

图片

 4. 旧数组首和新数组尾对比,直到Vnode不同

图片

 前面4步对比完成后,会有下面三种情况:

(1)旧数组没有剩余元素

图片

针对这种情况,直接将新数组中新增的元素插入到元素6后面

(2)新数组没有剩余元素

图片

针对这种情况,直接将旧数组中剩余的元素删除

(3)新旧数组都有剩余元素

图片

针对这种情况,外层遍历新数组剩余Vnode,内层遍历旧数组剩余Vnode,通过双层遍历找新Vnode对应的旧Vnode:

  1. 没有找到对应的旧节点,则直接创建新的DOM

  2. 找到对应的旧节点,直接复用旧的DOM,将变化的属性更改为新的值即可

Vue3 Diff算法

patchKeyedChildren

如果新老子元素都是数组的时候,需要先做首尾的预判,如果新的子元素和老的子元素在预判完毕后,未处理的元素依然是数组,那么就需要对两个数组计算diff,最终找到最短的操作路径,能够让老的子元素尽可能少的操作,更新成为新的子元素。

旧数组

let c1 = [{
    id: 'a_key',
    name: 'a'
  },
  {
    id: 'b_key',
    name: 'b'
  },
  {
    id: 'c_key',
    name: 'c'
  },
  {
    id: 'd_key',
    name: 'd'
  },
  {
    id: 'e_key',
    name: 'e'
  }
]

let c2 = [{
    id: 'c_key',
    name: 'c'
  },
  {
    id: 'b_key',
    name: 'b'
  },
  {
    id: 'e_key',
    name: 'e'
  },

  {
    id: 'd_key',
    name: 'd'
  },
  {
    id: 'a_key',
    name: 'a'
  },
]

建立新节点key与其下标的映射, 保存在keyToNewIndexMap中

  • keyToNewIndexMap计算源码如下:

// e2是c2的长度
const s1 = i;
const s2 = i;
const keyToNewIndexMap = /* @__PURE__ */ new Map();
for (i = s2; i <= e2; i++) { // 遍历首尾预判后的新节点数组
  const nextChild = c2[i] = optimized ? cloneIfMounted(c2[i]) : normalizeVNode(c2[i]);
  if (nextChild.key != null) {
    if (keyToNewIndexMap.has(nextChild.key)) {
      warn$1(
        `Duplicate keys found during update:`,
        JSON.stringify(nextChild.key),
        `Make sure keys are unique.`
      );
    }
    keyToNewIndexMap.set(nextChild.key, i);
  }
}

s2 = i = 0

第一遍循环:i=0,

  • 代码执行完后,keyToNewIndexMap的值如下:

new Map([
  [
    "c_key",
    0
  ],
  [
    "b_key",
    1
  ],
  [
    "e_key",
    2
  ],
  [
    "d_key",
    3
  ],
  [
    "a_key",
    4
  ]
])

keyToNewIndexMap每一项是一个对象,对象的key是新数组当前项的key(即id),对象的value是新数组当前项的index。

newIndexToOldIndexMap 记录新坐标到旧坐标的映射, 旧坐标是从1开始的。

 

js

let j;
let patched = 0; // 已经对比的数量
const toBePatched = e2 - s2 + 1; //需要对比的数量
let moved = false;
let maxNewIndexSoFar = 0;
const newIndexToOldIndexMap = new Array(toBePatched);
// 初始化newIndexToOldIndexMap
for (i = 0; i < toBePatched; i++)
  newIndexToOldIndexMap[i] = 0;

for (i = s1; i <= e1; i++) { // 遍历旧节点数组
  const prevChild = c1[i];

  // 新数组已经对比完了,将旧数组中对于的节点删除
  if (patched >= toBePatched) {
    unmount(prevChild, parentComponent, parentSuspense, true);
    continue;
  }

  // 找到同元素在新数组中的坐标
  let newIndex;
  if (prevChild.key != null) { // 元素存在key
    newIndex = keyToNewIndexMap.get(prevChild.key); // 通过key获取元素在新数组的坐标
  } else {
    // 没有key时,遍历新数组找到新坐标
    for (j = s2; j <= e2; j++) {
      if (newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j])) {
        newIndex = j;
        break;
      }
    }
  }
  if (newIndex === void 0) { // 新数组中没有找到当前遍历的旧元素,则删除这个旧元素
    unmount(prevChild, parentComponent, parentSuspense, true);
  } else {
    // 建立新坐标到旧坐标到映射
    newIndexToOldIndexMap[newIndex - s2] = i + 1;

    // 判断元素需要移动
    if (newIndex >= maxNewIndexSoFar) {
      maxNewIndexSoFar = newIndex;
    } else {
      moved = true;
    }
    patch(
      prevChild,
      c2[newIndex],
      container,
      null,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized
    );
    patched++;
  }
}

即新数组第1个元素在旧数组的坐标为3

[
  3,
  2,
  5,
  4,
  1
]

在v-for循环中为什么需要key,且不能为index?

通过key可以快速的匹配相同节点。没有key的时候需要遍历新节点数组查找,导致匹配相同节点耗时久。如果key是index,则会错误的匹配相同节点,导致DOM操作增加。

increasingNewIndexSequence最长递增子序列

  • 计算最长递增子序列源码

const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR;
function getSequence(arr) {
  const p2 = arr.slice(); // 反向链表, 用于后续回溯修正;记录前置指针
  const result = [0]; // 递增子序列,存的是坐标
  let i, j, u, v, c;
  const len = arr.length;
  for (i = 0; i < len; i++) { //遍历原数组
    const arrI = arr[i];
    if (arrI !== 0) {
      j = result[result.length - 1]; // 递增子序列的末尾坐标
      if (arr[j] < arrI) { // 对比递增子序列的最后一个元素和当前元素, 递增子序列最后一个元素小于当前元素,则将当前元素的坐标push到递增子序列中
        p2[i] = j;
        result.push(i);
        continue;
      }
      // 二分查找,找到递增子序列中第一个比当前元素大的值
      u = 0;
      v = result.length - 1;
      while (u < v) {
        c = u + v >> 1; // 取递增子序列的中位数
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c;
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p2[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  // 回溯修正
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p2[v];
  }
  return result;
}
  • 最长递增子序列用到的算法

    动态规划、贪心算法、二分查找、反向链表、回溯修正

  • 计算后的结果

increasingNewIndexSequence = [1,3]

如果有移动,则执行下面代码

j = increasingNewIndexSequence.length - 1;
for (i = toBePatched - 1; i >= 0; i--) { // 从后往前处理剩余新节点数组
  const nextIndex = s2 + i;
  const nextChild = c2[nextIndex];
  const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
  if (newIndexToOldIndexMap[i] === 0) { //新增节点
    patch(
      null,
      nextChild,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized
    );
  } else if (moved) {
    if (j < 0 || i !== increasingNewIndexSequence[j]) {
      move(nextChild, container, anchor, 2); // 执行移动操作,将nextChild移动到anchor的前面
    } else {
      j--;
    }
  }
}

新坐标到旧坐标的映射[3,2,5,4,1], 新坐标1和3保持不动,

旧坐标0(1-0)的节点移动到最末的位置,即将key为a_key的元素移动到最末的位置

图片

旧坐标4(5-1)的节点移动到新坐标2的位置,即将key为e_key的元素移动到d_key的前面

图片

旧坐标2(3-1)的节点移动到新坐标0的位置,即将key为c_key的元素移动到b_key的前面

图片

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

勒布朗-前端

请多多支持,留点爱心早餐

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值