比对一下react,vue2.x,vue3.x的diff算法

从0写一下diff算法,我是一边写代码,一边写文章,整理一下思路。

注:这里只讨论tag属性相同并且多个children的情况,
不相同的tag直接替换,删除,这没啥好写的。

用这个例子来说明

export const Hello = {
  name: 'Hello',
  data() {
    return {
      childList: ['a', 'b', 'c'],
    };
  },
  mounted() {
    setTimeout(() => {
      this.childList = this.childList.reverse();
    }, 1000);
  },
  render: function(h) {
    return h('ul', {
    }, this.childList.map((child) => h('li', {}, child)));
  },
};

简单diff,把原有的删掉,把更新后的插入

function updateChildren (elm, prevChildren, nextChildren) {
  // 删除旧节点
  for (const child of prevChildren) {
    elm.removeChild(child.elm);
  }
// 把新的vnode插入
  for (const child of nextChildren) {
    createElm(child, elm); //vnode生成正式dom
  }
}

变化前后的标签都是li,所以只用比对vnodeData和children即可,复用原有的DOM。

先只从这个例子出发
我只用遍历旧的vnode,然后把旧的vnode和新的vnode patch就行

function updateChildren (elm, prevChildren, nextChildren) {
  for (let i = 0; i < prevChildren.length; i++) {
    patchVnode(nextChildren[i], prevChildren[i]);
  }
}

这样就省掉移除和新增dom的开销
现在的问题是,我的例子刚好是新旧vnode数量一样,如果不一样就有问题
示例改成这样:

// 新的children vnode增加了一个d
export const Hello1 = {
  name: 'Hello1',
  data() {
    return {
      childList: ['a', 'b', 'c'],
    };
  },
  mounted() {
    setTimeout(() => {
      this.childList.reverse().push('d');
    }, 1000);
  },
  render: function(h) {
    return h('ul', {
    }, this.childList.map((child) => h('li', {}, child)));
  },
};
// 新的的children vnode删除了一个元素
export const Hello2 = {
  name: 'Hello2',
  data() {
    return {
      childList: ['a', 'b', 'c'],
    };
  },
  mounted() {
    setTimeout(() => {
      this.childList.reverse().shift();
    }, 1000);
  },
  render: function(h) {
    return h('ul', {
    }, this.childList.map((child) => h('li', {}, child)));
  },
};

实现思路改成:先看看是旧的长度长,还是新的长,如果旧的长,我就遍历新的,然后把多出来的旧节点删掉,如果新的长,我就遍历旧的,然后多出来的新vnode加上。

function updateChildren (elm, prevChildren, nextChildren) {
  const oldLen = prevChildren.length;
  const newLen = nextChildren.length;
  const commonLen = oldLen > newLen ? newLen : oldLen;
  
  for (let i; i < commonLen.length; i++) {
    patchVnode(nextChildren[i], prevChildren[i]);
  }
  if (oldLen < newLen) {
      createElm(child, [], elm);
    }
  } else {
    for (const child of prevChildren.slice(oldLen - (oldLen - newLen))) {
      elm.removeChild(child.elm);
    }
  }
}

仍然有可优化的空间,还是下面这幅图



通过我们上面的diff算法,实现的过程会比对 preve vnode和next vnode,标签相同,则只用比对vnodedata和children。发现

  • 标签的子节点(文本节点a,b,c)不同,于是分别删除文本节点a,b,c,然后重新生成新的文本节点c,b,a。但是实际上这几个
  • 只是位置不同,那优化的方案就是复用已经生成的dom,把它移动到正确的位置。

    怎么移动?我们使用key来将新旧vnode做一次映射。

    首先我们找到可以复用的vnode
    可以做两次遍历,外层遍历next vnode,内层遍历prev vnode

    for (let i = 0; i < nextChildren.length; i++) {
        const nextVnode = nextChildren[i];
        for (let j = 0; j < prevChildren.length; j++) {
          const prevVnode = prevChildren[j];
          if (nextVnode.key === prevVnode.key) {
            // 说明可以复用
            patchVnode(nextVnode, prevVnode);
          }
        }  
      }
    

    如果next vnode和prev vnode只是位置移动,vnodedata和children没有任何变动,调用patchVnode之后不会有任何dom操作。
    接下来只需要把这个key相同的vnode移动到正确的位置即可。我们的问题变成了怎么移动。
    首先需要知道两个事情:

    1. 每一个prev vnode都引用了一个真实dom节点,每个next vnode这个时候都没有真实dom节点。
    2. 调用patchVnode的时候会把prevVnode引用的真实Dom的引用赋值给nextVnode,就像这样
    const elm = vnode.elm = oldVnode.elm;
    

    还是拿上面的例子,外层遍历next vnode,
    遍历第一个元素的时候, 第一个vnode是li(c),然后去prev vnode里找,在最后一个节点找到了,这里外层是第一个元素,不做任何移动的操作,我们记录一下这个vnode在prevVnode中的索引位置lastIndex,接下来在遍历的时候,如果j 这里多说一句,dom操作的api里,只有insertBefore(),没有insertAfter()。也就是说只有把某个dom插入到某个元素前面这个方法,没有插入到某个元素后面这个方法,所以我们只能用insertBefore()。那么思路就变成了,当j 当j>=lastIndex的时候,说明这个顺序是正确的的,不用移动,然后把lastIndex = j;
    也就是说,只把prevVnode中后面的元素往前移动,原本顺序是正确的就不变。
    现在我们的diff的代码变成了这样

    function updateChildren (elm, prevChildren, nextChildren) {
      let lastIndex = 0;
      for (let i = 0; i < nextChildren.length; i++) {
        const nextVnode = nextChildren[i];
        console.log('nextVnode: ', nextVnode);
        for (let j = 0; j < prevChildren.length; j++) {
          const prevVnode = prevChildren[j];
          if (nextVnode.key === prevVnode.key) {
            patchVnode(nextVnode, prevVnode);
            if (j < lastIndex) {
              elm.insertBefore(prevVnode.elm, nextChildren[i-1].elm.nextSibling);
            } else {
              lastIndex = j;
            }
          }
        }  
      }
    }
    

    同样的问题,如果新旧vnode的元素数量一样,那就已经可以工作了。接下来要做的就是新增节点和删除节点。
    新增节点:
    整个框架中将vnode挂载到真实dom上都调用patch函数,patch里调用createElm来生成真实dom。按照上面的实现,如果nextVnode中有一个节点是prevVnode中没有的,就有问题



    在prevVnode中找不到li(d),那我们需要调用createElm挂在这个新的节点,因为这里的节点需要超入到li(b)和li(c)之间,所以需要用insertBefore()。在每次遍历nextVnode的时候用一个变量find=false表示是否能够在prevVnode中找到节点,如果找到了就find=true。如果内层遍历后find是false,那说明这是一个新的节点

    function updateChildren (elm, prevChildren, nextChildren) {
      let lastIndex = 0;
      for (let i = 0; i < nextChildren.length; i++) {
        let find = false;
        const nextVnode = nextChildren[i];
        for (let j = 0; j < prevChildren.length; j++) {
          const prevVnode = prevChildren[j];
          if (nextVnode.key === prevVnode.key) {
            find = true;
            patchVnode(nextVnode, prevVnode);
            if (j < lastIndex) {
              elm.insertBefore(prevVnode.elm, nextChildren[i-1].elm.nextSibling);
            } else {
              lastIndex = j;
            }
          }
        } 
        if (!find) {
          createElm(nextChildren[i], elm, nextChildren[i-1].elm.nextSibling);
        }
      }
    }
    

    我们的createElm函数需要判断一下第四个参数,如果没有就是用appendChild直接把元素放到父节点的最后,如果有第四个参数,则需要调用insertBefore来插入到正确的位置。

    接下来要做的是删除prevVnode多余节点:



    在nextVnode中已经没有li(d)了,我们需要在执行完上面所讲的所有流程后在遍历一次prevVnode,然后拿到nextVnode里去找,如果找不到相同key的节点,那就说明这个节点已经被删除了,我们直接用removeChild方法删除Dom

    function updateChildren (elm, prevChildren, nextChildren) {
      let lastIndex = 0;
      for (let i = 0; i < nextChildren.length; i++) {
        let find = false;
        const nextVnode = nextChildren[i];
        for (let j = 0; j < prevChildren.length; j++) {
          const prevVnode = prevChildren[j];
          if (nextVnode.key === prevVnode.key) {
            find = true;
            patchVnode(nextVnode, prevVnode);
            if (j < lastIndex) {
              elm.insertBefore(prevVnode.elm, nextChildren[i-1].elm.nextSibling);
            } else {
              lastIndex = j;
            }
          }
        } 
        if (!find) {
          createElm(nextChildren[i], [], elm, nextChildren[i-1].elm.nextSibling);
        }
      }
      for (const vnode of prevChildren) {
        const has = nextChildren.find(child => child.key === vnode.key);
        if (!has) {
          elm.removeChild(vnode.elm);
        }
      }
    }
    

    完整的代码:https://github.com/TingYinHelen/tempo/blob/main/src/platforms/web/patch.js
    在react-diff分支(目前有可能代码仓库还没有开源,等我实现更完善的时候会开源出来,项目结构可能有变化,看tempo仓库就行)

    这里我的代码实现的diff算法很明显看出来时间复杂度是O(n2)。那么这里在算法上依然又可以优化的空间,这里我把nextChildren和prevChildren都设计成了数组的类型,这里可以把nextChildren、prevChildren设计成对象类型,用户传入的key作为对象的key,把vnode作为对象的value,这样就可以只循环nextChildren,然后通过prevChildren[key]的方式找到prevChidren中可复用的dom。这样就可以把时间复杂度降到O(n)。
    以上就是react的diff算法的实现

    下面来讲一下vue2的diff算法
    先说一下上面代码的问题,举个例子,下面这个情况


    如果按照react的方法,整个过程会移动2次:
    li(c)是第一个节点,不需要移动,lastIndex=2
    li(b), j=1, j li(a), j=0, j

    但是通过肉眼来看,其实只用把li(c)移动到第一个就行,只需要移动1一次。
    于是vue2这么来设计的


    首先找到四个节点vnode:prev的第一个,next的第一个,prev的最后一个,next的最后一个,然后分别把这四个节点作比对:1. 把prev的第一个节点和next的第一个比对;2. 把prev的最后一个和next的最后一个比对;3.prev的第一个和next的最后一个;4. next的第一个和prev的最后一个。如果找到相同key的vnode,就做移动,移动后把前面的指针往后移动,后面的指针往前移动,直到前后的指针重合,如果key不相同就只patch更新vnodedata和children。下面来走一下流程

    1. li(a)和li(b),key不同,只patch,不移动
    2. li(d)和li(c),key不同,只patch,不移动
    3. li(a)和li(c),key不同,只patch,不移动
    4. li(d)和li(d),key相同,先patch,需要移动移动,移动的方法就是把prev的li(d)移动到li(a)的前面。然后移动指针,因为prev的最后一个做了移动,所以把prev的指向后面的指针往前移动一个,因为next的第一个vnode已经找到了对应的dom,所以next的前面的指针往后移动一个。现在比对的图变成了下面这样:



      这个时候的真实DOM


    继续比对

    1. li(a)和li(b),key不同,只patch,不移动
    2. li(c)和li(c),相同相同,先patch,因为next的最后一个元素也刚好是prev的最后一个,所以不移动,prev和next都往前移动指针
      这个时候真实DOM:



      现在最新的比对图:



      继续比对
    3. li(a)和li(b),key不同,只patch,不移动
    4. li(b)和li(a),key不同,只patch,不移动
    5. li(a) 和li (a),key相同,patch,把prev的li(a)移动到next的后面指针的元素的后面
      真实的DOM变成了这样


    比对的图变成这样



    继续比对:
    li(b)和li(b)的key相同,patch,都是前指针相同所以不移动,移动指针
    这个时候前指针就在后指针后面了,这个比对就结束了。

    function updateChildren (elm, prevChildren, nextChildren) {
      let oldStartIndex = 0;
      let oldEndIndex = prevChildren.length - 1;
      let newStartIndex = 0;
      let newEndIndex = nextChildren.length - 1;
    
      while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        const oldStartVnode = prevChildren[oldStartIndex];
        const oldEndVnode = prevChildren[oldEndIndex];
        const newStartVnode = nextChildren[newStartIndex];
        const newEndVnode = nextChildren[newEndIndex];
    
        if (oldStartVnode.key === newStartVnode.key) {
          patchVnode(newStartVnode, oldStartVnode);
          oldStartIndex++;
          newStartIndex++;
        } else if (oldEndVnode.key === newEndVnode.key) {
          patchVnode(newEndVnode, oldEndVnode);
          oldEndIndex--;
          newEndIndex--;
        } else if (oldStartVnode.key === newEndVnode.key) {
          patchVnode(newEndVnode, oldStartVnode);
          elm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
          newEndIndex--;
          oldStartIndex++;
        } else if (oldEndVnode.key === newStartVnode.key) {
          patchVnode(newStartVnode, oldEndVnode);
          elm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
          oldEndIndex--;
          newStartIndex++;
        }
      }
    }
    

    这就完成了常规的比对,还有不常规的,如下图:



    经过1,2,3,4次比对后发现,没有相同的key值能够移动。
    这种情况我们没有办法,只有用老办法,用newStartIndex的key拿去依次到prev里的vnode,直到找到相同key值的老的vnode,先patch,然后获取真实dom移动到正确的位置(放到oldStartIndex前面),然后在prevChildren中把移动过后的vnode设置为undefined,在下次指针移动到这里的时候直接跳过,并且next的start指针向右移动。

    function updateChildren (elm, prevChildren, nextChildren) {
      let oldStartIndex = 0;
      let oldEndIndex = prevChildren.length - 1;
      let newStartIndex = 0;
      let newEndIndex = nextChildren.length - 1;
    
      while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        let oldStartVnode = prevChildren[oldStartIndex];
        let oldEndVnode = prevChildren[oldEndIndex];
        let newStartVnode = nextChildren[newStartIndex];
        let newEndVnode = nextChildren[newEndIndex];
    
        if (oldStartVnode === undefined) {
          oldStartVnode = prevChildren[++oldStartIndex];
        }
        if (oldEndVnode === undefined) {
          oldEndVnode = prevChildren[--oldEndIndex];
        }
    
        if (oldStartVnode.key === newStartVnode.key) {
          patchVnode(newStartVnode, oldStartVnode);
          oldStartIndex++;
          newStartIndex++;
        } else if (oldEndVnode.key === newEndVnode.key) {
          patchVnode(newEndVnode, oldEndVnode);
          oldEndIndex--;
          newEndIndex--;
        } else if (oldStartVnode.key === newEndVnode.key) {
          patchVnode(newEndVnode, oldStartVnode);
          elm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
          newEndIndex--;
          oldStartIndex++;
        } else if (oldEndVnode.key === newStartVnode.key) {
          patchVnode(newStartVnode, oldEndVnode);
          elm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
          oldEndIndex--;
          newStartIndex++;
        } else {
          const idxInOld = prevChildren.findIndex(child => child.key === newStartVnode.key);
          if (idxInOld >= 0) {
            elm.insertBefore(prevChildren[idxInOld].elm, oldStartVnode.elm);
            prevChildren[idxInOld] = undefined;
            newStartIndex++;
          }
        }
      }
    }
    

    接下来就是新增节点


    image.png

    这种排列方法,按照上面的方法,经过1,2,3,4比对后找不到相同key,然后然后用newStartIndex到老的vnode中去找,仍然找不着,这个时候说明是一个新节点,把它插入到oldStartIndex前面

    const idxInOld = prevChildren.findIndex(child => child.key === newStartVnode.key);
          if (idxInOld >= 0) {
            elm.insertBefore(prevChildren[idxInOld].elm, oldStartVnode.elm);
            prevChildren[idxInOld] = undefined;
          } else {
            createElm(newStartVnode, [], elm, oldStartVnode.elm);
          }
          newStartIndex++;
    

    最后一步是移除多余dom
    还是按照原来步骤进行比对


    也就是说当newStartIndex > newEndIndex的时候就说明有dom需要删除,删除的就是oldStartIndex 到 oldEndIndex。

    if (oldEndIndex < oldStartIndex) {
        for (let i = newStartIndex; i <= newEndIndex; i++) {
          createElm(nextChildren[i], elm, oldStartVnode.elm);
        }
      } else if (newEndIndex < newStartIndex) {
        for (let i = oldStartIndex; i <= oldEndIndex; i++) {
          elm.removeChild(prevChildren[i].elm);
        }
      }
    

    完整代码
    完整的代码:https://github.com/TingYinHelen/tempo/blob/main/src/platforms/web/patch.js
    vue2-diff分支

    下面来讲vue3的diff算法
    vue3首先对chidren做了预处理,预处理其实就是去除相同的前缀和后缀,之比较不相同的部分
    例如字符串:
    abcd
    abed
    去除前面相同部分和后面相同部分,剩下不同的部分就是
    c
    e
    应用到我们的children上也是同样的,我们先实现最简单情况


    image.png

    通过上图可以看出,前面a是相同的,后面e,f是相同的,去除前后相同的我们只用把中间新增的vnode insert进来就行。在预处理阶段需要4个指针,分别指向prev的第一个和next的第一个,在前缀比较的时候,如果key相同,则只patch,然后把这两个指针依次向后移动。同样需要两个指针分别指向prev和next的最后一个vnode,如果两个vnode key相同,只patch,指针向前移动。
    我们需要3个变量:j(指向前缀,因为prev和next前缀比较的时候索引值相同,所以只需要一个变量),prevEnd(指向prev的后缀),nextEnd(指向next的后缀)。



    前缀a相同,j++,j=1的时候prev的元素是e,next的c这时key不同,这个循环结束。


    接着循环遍历后缀,第一次prevEnd=2,nextEnd=3相同,指针向前移动,prevEnd=1,nextEnd=2,key还是相同,指向向前移动,prevEnd=0,nextEnd=1,key不同,循环结束


    当prevEnd < j,说明next有元素需要新增,当nextEnd == j,说明next的j需要被新增

    来看一下删除


    首先遍历前缀,a的key相同,j++,j=1,b和c的key不同,前缀循环结束。后缀c的key相同,向前移动指针,这个时候的图变成这样


    如果j>nextEnd,就说明有元素需要删除

    function updateChildren (elm, prevChildren, nextChildren) {
      let j = 0;
      let prevEnd = prevChildren.length - 1;
      let nextEnd = nextChildren.length - 1;
    
      while (prevChildren[j].key === nextChildren[j].key) {
        patchVnode(nextChildren[j], prevChildren[j]);
        j++;
      }
      while (prevChildren[prevEnd].key === nextChildren[nextEnd].key) {
        patchVnode(nextChildren[nextEnd--], prevChildren[prevEnd--]);
      }
    
      if (prevEnd < j || nextEnd === j) {
        while (j <= nextEnd) {
          createElm (nextChildren[j--], elm, prevChildren[prevEnd + 1].elm);
        }
      } else if (j > nextEnd) {
        while (j <= prevEnd) {
          elm.removeChild(prevChildren[j++].elm);
        }
      }
    }
    

    还有一种情况,如果在第一次循环的阶段,j大于了nextEnd,那说明nextChildren整个都已经patch完,就可以不用在家进行后缀的遍历,如果j大于了prevEnd,说明prevChildren整个都patch完成,也不用在继续第二次循环;同样,在第二次循环的时候,有上面说的情况也不用再继续执行。出于性能考虑,我们应该避免没有必要的代码执行。

    outer: {
        while (prevChildren[j].key === nextChildren[j].key) {
          patchVnode(nextChildren[j], prevChildren[j]);
          j++;
          if (j > nextEnd || j > prevEnd) {
            break outer;
          }
        }
        while (prevChildren[prevEnd].key === nextChildren[nextEnd].key) {
          patchVnode(nextChildren[nextEnd--], prevChildren[prevEnd--]);
          if (j > nextEnd || j > prevEnd) {
            break outer;
          }
        }
      }
    

    以上讨论的是预处理时新增和删除的特殊情况。大多数情况并不能在预处理就结束比对,所以下面来讨论常规情况。
    通过上面的分析,可以知道无论什么框架,diff算法的比对步骤是:先看有哪些vnode需要移动,然后考虑怎么移动,最后考虑新增和删除的情况。

    看一下下面这种情况


    通过预处理之后,a和e已经被比对,这个时候j 给一个source数组,source的长度等于预处理之后nextChildren剩余未处理节点的长度,source元素的初始值都是-1



    这个source数组的作用是用来存储,新的children中的元素在老的chidren中的位置:


    const prevStart = j;
    const nextStart = j;
    // 遍历旧的children
    for (let i = prevStart; i <= prevEnd; i++) {
      const prevVnode = prevChildren[i];
      // 遍历新的children
      for (let k = nextStart; k <= nextEnd; k++) {
        const nextVnode = nextChildren[k];
        // 找到拥有相同key值的vnode
        if (prevVnode.key === nextVnode.key) {
          // patch更新
          patch(nextVnode, prevVnode);
          source[k - nextStart] = i;
        }
      }
    }
    

    k代表的是在新的children中,遇到的节点的位置索引,用一个pos变量用来存储当遇到key相同的vnode的时候,遇到的索引的最大值,一旦发现后面遇到的索引值比之前的要小,则说明需要移动,这时我们用一个变量move来记录是否需要移动move=true,如果后面遇到的k比pos更大,则把k赋值给pos。这里的思路和react类似。
    这里之前已经说过,这里的时间复杂度是O(n2)。这里来优化一下
    我们可以为新的children节点,构建一个key到位置索引索引表。



    Index Map中的键是节点的key,值是节点是在新的children中的位置索引,由于数据结构带来的优势,使我们可以快速的定位旧的children中的节点在新的children中的位置。

        const prevStart = j;
        const nextStart = j;
        let pos = 0;
        let move = false;
        const keyIndex = {};
        for (let i = nextStart; i < nextStart; i++) {
          keyIndex[nextChildren[i].key] = i;
        }
        // 遍历旧的children的剩余未处理节点
        for (let i = prevStart; i <= prevEnd; i++) {
          const prevVnode = prevChildren[i];
          // 通过索引表快速找到新的children中key值相等的vnode的位置
          const k = keyIndex[prevVnode.key]
          if (k !== undefined) {
            patch(nextChildren[k], prevVnode);
            // 更新source数组
            source[k - nextStart] = i;
            if (pos > k) {
              move = true;
            } else {
              pos = k;
            }
          } else {
            // 删除节点
          }
        }
    

    现在的时间复杂度就是O(n)了。接下来可以做操作Dom的操作了。
    上面的逻辑中如果k是undefined,这里我们用prevChildren中的vnode在新的children中找,找不着,那么就说明这是一个多余的vnode,需要删除

        const prevStart = j;
        const nextStart = j;
        let pos = 0;
        let move = false;
        const keyIndex = {};
        for (let i = nextStart; i < nextStart; i++) {
          keyIndex[nextChildren[i].key] = i;
        }
        // 遍历旧的children的剩余未处理节点
        for (let i = prevStart; i <= prevEnd; i++) {
          const prevVnode = prevChildren[i];
          // 通过索引表快速找到新的children中key值相等的vnode的位置
          const k = keyIndex[prevVnode.key]
          if (k !== undefined) {
            patch(nextChildren[k], prevVnode);
            // 更新source数组
            source[k - nextStart] = i;
            if (pos > k) {
              move = true;
            } else {
              pos = k;
            }
          } else {
            elm.removeChild(prevChildren[i]);
          }
        }
    

    接下来做移动操作

    if (move) {
      ...
    }
    

    首先要做的是根据source计算一个最长递增子序列。

    if (move) {
      const seq = lis(source) // 
    }
    

    什么是最长递增子序列:
    给定一个数值序列,找到他的一个子序列,并且子序列中的值是递增的,子序列中的元素在原序列中不一定连续。
    比如给定序列:[0,8,4,12]
    那么他的最长递增子序列:[0,8,12]
    当然答案可能有多种:[0,4,12]

    我们调用lis函数后,求出数组source的最长递增子序列是[0,1]。注:这里lis函数求的是source中最长递增子序列的索引值。



    [0,1]告诉我们nextChildren的未处理节点中,位于位置0和位置1的节点,与他们在prevChildren中的先后顺序是没变的,在位置0 和位置1的节点是不需要移动的。

    i和j分别指向新的children中剩余未处理节点的最后一个节点,和最长子序列的的最后一个,并从后往前遍历。

    if (move) {
          const seq = lis(source);
          // j指向最长递增子序列的最后一个
          let j = seq.length - 1;
          // 从后往前遍历新children中的剩余未处理节点
          for (let i = nextLeft - 1; i > 0; i--) {
            if (i !== seq[j]) {
              // 移动
            } else {
              j--;
            }
          }
        }
    

    i的值的范围是0到nextLeft-1,等价于我们队剩余节点重新编号,接着看当前节点的索引是否与子序列中j相等。同时还需要注意source[i]是否为-1,是-1的节点需要新增。

    if (move) {
          const seq = lis(source);
          // j指向最长递增子序列的最后一个
          let j = seq.length - 1;
          // 从后往前遍历新children中的剩余未处理节点
          for (let i = nextLeft - 1; i > 0; i--) {
            if (source[i] === -1) {
              // 该节点在新 children 中的真实位置索引
              const pos = i + nextStart;
              const nextVNode = nextChildren[pos]
              const nextPos = pos + 1;
              createElm(nextVNode, [], elm, nextChildren[nextPos].elm);
    
            } else if (i !== seq[j]) {
              // 该节点在新 children 中的真实位置索引
              const pos = i + nextStart;
              const nextVNode = nextChildren[pos];
              // 该节点下一个节点的位置索引
              const nextPos = pos + 1;
              // 移动
              elm.insertBefore(
                nextVNode.el,
                nextPos < nextChildren.length
                  ? nextChildren[nextPos].elm
                  : null
              )
            } else {
              j--;
            }
          }
        }
    
  • 你可能感兴趣的:(比对一下react,vue2.x,vue3.x的diff算法)