27-更新element-children-2

135 阅读2分钟

对比情况

array -> array

  1. 新旧左侧对比,取出差异数据下标
  2. 新旧右侧对比,锁定右侧差异位置
  3. 新旧数据对比,新增少删

例子

参考 上一篇 中的 ArrayToArray.js

前导思考

image.png

Vue 的 diff中最重要的就是乱序部分的更新,要确定更新部分的位置,及途中绿框部分。Vue的diff做了几步

  • 第一步,首部diff。从新老数据的下标0的位置开始对比,即a开始,对比到c、e发现两者不同,停止首部diff,记录当前差异位置
  • 第二部,尾部diff。从新老数据末端开始,即g开始,对比到e、c发现两者不同,停止尾部diff,记录当前差异位置
  • 第三部,确立差异数据块,做替换。

代码实现

几个核心参数

  • el: 右侧老数据的差异位置
  • e2: 右侧新数据的差异位置
  • i: 左侧差异位置

头部对比

例子

// (a b) c
// (a b) d e

const prevChildren = [
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'C' }, 'C'),
]
const nextChildren = [
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'D' }, 'D'),
  h('p', { key: 'E' }, 'E'),
]

实现

function patchChildren(
    n1: any,
    n2: any,
    container: any,
    parentComponent: any,
  ) {
  // other code
  
   if (n2ShapeFlags === ShapeFlags.TEXT_CHILDREN) {
        /** array -> text
         * 1. 删除子元素
         * 2. 重新赋值
         */
        unmountChildren(n1Child);
        hostSetElementText(el, n2Child);
   } else {
        patchKeyedChildren(
          n1Child,
          n2Child,
          container,
          parentComponent
        );
   }
 }
 
function patchKeyedChildren(
    c1,
    c2,
    container,
    parentComponent,
  ) {
    /** array -> array
     * 1. 左侧对比,取出差异目标下标
     * 2. 右侧对比,锁定当前差异右侧位置
     * 3. 新老对比,新增少删
     */
    let e1 = c1.length - 1;
    let e2 = c2.length - 1;
    let i = 0;

    // 对比是否相同数据
    function isSameVNode(c1, c2) {
      return c1.key === c2.key && c1.type === c2.type;
    }
    /** 1. 左侧对比,取出左侧差异目标下标
     * 循环的边界在两个数组长度内
     * 从左到右对比,判断相同,继续向右查询,直至差异结束
     */
    while (i <= e1 && i <= e2) {
      if (isSameVNode(c1[i], c2[i])) {
        // 相同,再深度patch
        patch(c1[i], c2[i], container, parentComponent);
      } else {
        // 有差异,退出循环
        break;
      }
      i++;
    }
    console.log("左侧差异的位置", i);

  }

判断的时候用到了 key ,需要拓展一下对应的函数 vnode.ts

export function createdVNode(type, props?, children?) {
  // 将根组件转换为vnode,再将其暴露
  const vnode = {
    type,
    props,
    children,
    key: props && props.key,
  };

  return vnode;
}

h.ts

export function h(type, props?, children?) {
  return createdVNode(type, props, children);
}

结合图片消化

vue-diff-01.gif

尾部对比

例子

// a (b c)
// d e (b c)

const prevChildren = [
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'C' }, 'C'),
]
const nextChildren = [
  h('p', { key: 'D' }, 'D'),
  h('p', { key: 'E' }, 'E'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'C' }, 'C'),
]

实现

function patchKeyedChildren(
    c1,
    c2,
    container,
    parentComponent
  ) {
     // 1. 左侧对比
     // other code
     
     /** 2. 右侧对比,锁定右侧差异目标
     * 循环的边界在**左侧差异位置i**到**两个数组长度**之间
     * 在尾部开始判断,所以取的是对应children长度
     * 从右到左对比,判断相同,继续向左查询,直至差异
     */
    while (i <= e1 && i <= e2) {
      if (isSameVNode(c1[e1], c2[e2])) {
        // 相同,再深度patch
        patch(
          c1[e1],
          c2[e2],
          container,
          parentComponent
        );
      } else {
        // 有差异,退出循环
        break;
      }
      e1--;
      e2--;
    }

    console.log("右侧差异位置-旧的", e1);
    console.log("右侧差异位置-新的", e2);

}

结合图片消化

vue-diff-02.gif

新老数据对比-新增

例子

新数据在尾部

// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]
const nextChildren = [
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'C' }, 'C'),
  h('p', { key: 'D' }, 'D'),
]

新数据在头部

// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]
const nextChildren = [
  h('p', { key: 'C' }, 'C'),
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
]

实现

改写insert,新增了 anchor 入参,因为之前的insert只支持数据插入到最后,不能插入到对应位置,所以我们需要给一个对应的anchor,把数据插入到对应的锚点位置

export function insert(el, parent, anchor) {
  /** 根据锚点插入到对应位置
   * 1. anchor为null默认插到尾部
   * 2. anchor不为空,则插到anchor对应的元素之前
   */
  parent.insertBefore(el, anchor || null);
}

实现头部、尾部数据插入

function patchKeyedChildren(
    c1,
    c2,
    container,
    parentComponent,
    parentAnchor
  ) {
     // 1. 左侧对比
     // 2. 右侧对比
     // other code
     
      /** 3. 新的比旧的长,添加元素
     * 1. 改写insert,支持插入到对应位置
     * 2. i 为新老数据左侧的差异位置,e1、e2为数据右侧的差异位置
     * 3. i > e1,说明新的比旧的长,需要插入数据
     * 4. i > e2,说明新的比旧的短,需要删除数据
     */
     
    const l2 = c2.length - 1;

    /** 插入数据
     * 1. 左侧 i 大于 e1,说明新数据比旧数据多,要把新数据插入
     * 2. 添加范围在新数据长度内
     */
    if (i > e1 && i <= e2) {
      /** nextPos用来判断插入数据的位置
       * 1. nextPos为新数据差异位的后一个元素的锚点位置
       * 2. 如果锚点超出新数据children长度,则没有找到对应的锚点元素,则插到尾部
       * 3. 如果锚点在新数据children长度范围内,则取到对应的下标元素作为锚点元素,插到对应的位置
       */
      const nextPos = e2 + 1;
      const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
      while (i <= e2) {
        patch(null, c2[i], container, parentComponent, anchor);
        i += 1;
      }
    }
     
}

新老数据对比-删除

例子

尾部删除

// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
const prevChildren = [
  h('p', { key: 'A' }, 'A'),
  h('p', { key: 'B' }, 'B'),
  h('p', { key: 'C' }, 'C'),
]
const nextChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]

头部删除

// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1

const prevChildren = [
  h("p", { key: "A" }, "A"),
  h("p", { key: "B" }, "B"),
  h("p", { key: "C" }, "C"),
];
const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];

实现

function patchKeyedChildren(
    c1,
    c2,
    container,
    parentComponent,
    parentAnchor
  ) {
     // 1. 左侧对比
     // 2. 右侧对比
     // other code
     
      /** 3. 新的比旧的长,添加元素
     * 1. 改写insert,支持插入到对应位置
     * 2. i 为新老数据左侧的差异位置,e1、e2为数据右侧的差异位置
     * 3. i > e1,说明新的比旧的长,需要插入数据
     * 4. i > e2,说明新的比旧的短,需要删除数据
     */
     
    const l2 = c2.length - 1;

    /** 插入数据
     * 1. 左侧 i 大于 e1,说明新数据比旧数据多,要把新数据插入
     * 2. 添加范围在新数据长度内
     */
    if (i > e1 && i <= e2) {
      // other code
    }else if (i > e2 && i <= e1) {
      /** 删除数据
       * 1. 左侧 i 大于 e2,则新数据比旧数据少,删除对应数据
       * 2. 删除范围在旧数据的长度内
       */
      while (i <= e1) {
        hostRemove(c1[i].el);
        i += 1;
      }
    }
     
}