比对一下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。发现<li>标签的子节点(文本节点a,b,c)不同,于是分别删除文本节点a,b,c,然后重新生成新的文本节点c,b,a。但是实际上这几个<li>只是位置不同,那优化的方案就是复用已经生成的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<lastIndex,说明原本prevVnode在前面的元素,在nextVnode中变到了后面来了,那么我们就把prevVnode[j]放到nextVnode[i-1]的后面。
这里多说一句,dom操作的api里,只有insertBefore(),没有insertAfter()。也就是说只有把某个dom插入到某个元素前面这个方法,没有插入到某个元素后面这个方法,所以我们只能用insertBefore()。那么思路就变成了,当j<lastIndex的时候,把prevChildren[j]插入到nextVnode[i-1]的真实dom的后面元素的前面。
当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<lastIndex, 移动到li(c)后面 (第1次移动)
li(a), j=0, j<lastIndex, 移动到li(b)后面 (第2次移动)

但是通过肉眼来看,其实只用把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<prevEnd并且j<nextEnd,所以我们上面的实现不满足,那么实现思路,
给一个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--;
        }
      }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容