Vue3源码:diff算法

95 阅读4分钟

效果

以下代码为例:

<script setup>
import { 
  reactive
} from "vue";

let numberList = reactive([
  {id: 1, number: 1000},
  {id: 2, number: 2000},
  {id: 3, number: 3000},
  {id: 4, number: 4000},
  {id: 5, number: 5000}
]);

function changeNum() {
  numberList[1] = {id: 6, number: 6000};
  numberList[3] = {id: 7, number: 7000};
  numberList[4] = {id: 8, number: 8000};
  numberList.push({id: 5, number: 5000});
}
</script>

<template>
  <div>
    <button @click="changeNum">changeNum</button>
    <ul>
      <li v-for="c in numberList" :key="c.id">{{ c.number }}</li>
    </ul>
  </div>
</template>

前后数据对比:

修改前:
{id: 1, number: 1000},
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}

修改后:
{id: 1, number: 1000},
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},
{id: 5, number: 5000},

过程

image.png 断点打在patchKeyedChildren
c1:老节点(vnode),c2:新节点;这里3个指针,i=0(从前往后遍历),e1=4、e2=5(从后往前遍历)

1、从前往后

image.png image.png c1[0] 和 c2[0] 判断是同一个节点,执行patch去比较节点内容并判断是否修改,i++;
当 i=1 的时候,c1[1]、c2[1] key值不同直接break;
此时还剩以下节点未比对

旧vnode:
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}

新vnode:
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},
{id: 5, number: 5000},
2、从后往前

image.png c1[4] 和 c2[5]是同一节点进入patch比对vnode内容,e1--、e2--;
e1=3、e2=4的时候不是同一节点直接break;
此时还剩以下节点未比对

旧vnode:
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},

新vnode:
{id: 6, number: 6000},
{id: 3, number: 3000},
{id: 7, number: 7000},
{id: 8, number: 8000},

3、乱序

经过前两次循环,新老vnode开头第一个和倒数第一个均已比对完毕后续不再考虑,现在3个指针i=1、e1=3,e2=4\

(1)找出旧vnode与新vnode key相同的节点

image.png 新vnode还剩4个节点未比对,将节点key与 数组索引 存放在keyToNewIndexMap中\

image.png 遍历旧vnode,找keyToNewIndexMap(新vnode key的map结构)中是否存在相同key;\

image.png 新vnode中没有id==2或4的,从dom结构中删除这个节点

image.png 有id==3的,用newIndexToOldIndexMap记录 旧vnode数组索引 和 新vnode数组索引 的映射关系,再执行patch\

此时还剩以下节点未比对

旧vnode:多余的已删除(注:vnode依然存在,只是从父dom结构中删除子dom)

新vnode:
{id: 6, number: 6000},
{id: 7, number: 7000},
{id: 8, number: 8000},

(2)添加旧vnode没有的节点

image.png 从后往前添加新vnode中没有对比过的节点,这里从id=8开始,anchor是锚点为了确认节点插在哪,此刻是id=5的节点;
newIndexToOldIndexMap[i] === 0判断该节点是否已经被旧节点相同key对比过了(这里id=3已经对比过了不是0)
至此diff算法已完成

最长递增子序列

写完博客才发现上面的例子过于简单没有触发这个算法
在乱序对比中,遍历旧vnode找到key与新vnode相同的会记录在newIndexToOldIndexMap中
以下为例,这个例子中所有节点都会复用。patch是将新旧节点内容对比,并没有改变真实dom的顺序如果不排序展示在页面上的还是id:1,2,3,4,5这个顺序,所以拿到最长递增子序列用来减少dom结构顺序调整消耗的性能

修改前:
{id: 1, number: 1000},
{id: 2, number: 2000},
{id: 3, number: 3000},
{id: 4, number: 4000},
{id: 5, number: 5000}

修改后:
{id: 4, number: 4000},
{id: 1, number: 1000},
{id: 5, number: 5000},
{id: 2, number: 2000},
{id: 3, number: 3000}

newIndexToOldIndexMap数组是 [4,1,5,2,3] image.png 通过getSequence求出数组最长递增子序列的索引,结果[1,3,4] (对应的是key值1,2,3,其他节点都根据这3个节点调整)
下面是源码用到的算法:

function getSequence(arr) {
  const p = 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) {
        p[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) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  return result;
}

image.png 按 [4,1,5,2,3] 从后往前遍历,遇到id=5或4的时候执行move