Vue2、Vue3的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,最终找到最短的操作路径,能够让老的子元素尽可能少的操作,更新成为新的子元素。

旧数组

js 复制代码
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计算源码如下:
js 复制代码
// 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的值如下:
js 复制代码
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

js 复制代码
[
    3,
    2,
    5,
    4,
    1
]

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

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

increasingNewIndexSequence最长递增子序列

  • 计算最长递增子序列源码
js 复制代码
const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR;
js 复制代码
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;
}
  • 最长递增子序列用到的算法

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

  • 计算后的结果

js 复制代码
increasingNewIndexSequence = [1,3]

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

  • 源码
js 复制代码
      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的前面

参考: www.bilibili.com/video/BV1u8...

相关推荐
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
尝尝你的优乐美6 小时前
vue3.0中h函数的简单使用
前端·javascript·vue.js
会发光的猪。7 小时前
如何使用脚手架创建一个若依框架vue3+setup+js+vite的项目详细教程
前端·javascript·vue.js·前端框架
别忘了微笑_cuicui8 小时前
vue中调用全屏方法、 elementUI弹框在全屏模式下不出现问题、多级嵌套弹框蒙层遮挡问题等处理与实现方案
前端·vue.js·elementui
计算机学姐9 小时前
基于Python的药房管理系统
开发语言·vue.js·后端·python·mysql·pycharm·django
放逐者-保持本心,方可放逐11 小时前
vue3 动态路由+动态组件+缓存应用
前端·vue.js·缓存
周末不下雨11 小时前
关于搭建前端的流程整理——node.js、cnpm、vue、初始化——创建webpack、安装依赖、激活
前端·vue.js·node.js
花开花落与云卷云舒11 小时前
新手 Vue 项目运行
前端·javascript·css·vue.js·前端框架·html·springboot