10-双端Diff

99 阅读10分钟

双端Diff算法

双端比较的原理

简单Diff算法的缺点

我们在之前,已经学习过简单Diff算法了,但是他的DOM移动操作并不是最优的

可以拿我们之前的例子分析一下:

新旧节点顺序改变.jpg

在这个例子中,我们之前对DOM元素的移动,移动的是p-1p-2,因为我们存储了lastIndex,在遍历到p-3的时候,我们会存储到最大lastIndex,所以在后面遍历p-1p-2的时候,由于都比lastIndex小,所以移动的是p-1p-2

而实际上,我们可以发现,这个移动方式并不是最优的,我们可以移动p-3,将其移动到真实DOM节点p-1即可,这样只需要移动一次就可以完成更新

但是目前的简单Diff算法做不到这一点,但是双端Diff算法可以

双端Diff算法的原理

双端Diff算法是一种同时对新旧两组子节点的两个端点进行比较的算法,所以我们需要四个索引值,分别指向新旧两组子节点的端点

双端Diff前提.jpg

使用一个函数来封装两组子节点的打补丁工作

其中,创建四个索引值,并且分别指向新旧两组子节点的头和尾节点

 function patchKeyedChildren(n1, n2, container) {
     const oldChildren = n1.children
     const newChildren = n2.children
     //四个索引值
     let oldStartIdx = 0
     let oldEndIdx = oldChildren.length - 1
     let newStartIdx = 0
     let newEndIdx = newChildren.length - 1
     //四个索引指向的vnode节点
     let oldStartVNode = oldChildren[oldStartIdx]
     let oldEndVNode = oldChildren[oldEndIdx]
     let newStartVNode = newChildren[newStartIdx]
     let newEndVNode = newChildren[newEndIdx]
 }

接下来,就应该进行双端比较了

双端比较.jpg

每一轮比较都分为四个步骤,详细分析如下:

  • 第一步,比较旧的一组子节点的第一个子节点p-1和新的一组子节点中的第一个子节点p-4,由于两者的key不相同,因此不相同,不能复用,于是啥都不做

  • 第二步,比较旧的一组子节点的最后一个子节点p-4和新的一组子节点中的最后一个子节点p-3,由于两者的key不相同,因此不相同,不可复用,于是啥都不做

  • 第三步,比较旧的一组子节点的第一个子节点p-1和新的一组子节点中的最后一个子节点p-3,由于两者的key不相同,因此不相同,不可复用,于是啥都不做

  • 第四步,比较旧的一组子节点的最后一个子节点p-4和新的一组子节点中的第一个子节点p-4,由于两者的key相同,所以可以进行DOM复用,而具体应该怎么进行复用呢?

    由于第四步是比较旧的一组子节点的最后一个子节点与新的一组子节点的第一个子节点,说明:节点p-4原本是最后一个子节点,但在新的顺序中,它变成了第一个子节点

    也就是将索引oldEndIdx指向的虚拟节点所对应的真实DOM移动到索引oldStartIdx指向的虚拟节点对应的真实DOM前面

     function patchKeyedChildren(n1, n2, container) {
         const oldChildren = n1.children
         const newChildren = n2.children
         //四个索引值
         let oldStartIdx = 0
         let oldEndIdx = oldChildren.length - 1
         let newStartIdx = 0
         let newEndIdx = newChildren.length - 1
         //四个索引指向的vnode节点
         let oldStartVNode = oldChildren[oldStartIdx]
         let oldEndVNode = oldChildren[oldEndIdx]
         let newStartVNode = newChildren[newStartIdx]
         let newEndVNode = newChildren[newEndIdx]
     ​
         if(oldStartVNode.key === newStartVNode.key){
     ​
         }else if(oldEndVNode.key === newEndVNode.key){
     ​
         }else if(oldStartVNode.key === newEndVNode.key){
     ​
         }else if(oldEndVNode.key === newStartVNode.key){
             //需要调用patch函数进行打补丁
             patch(oldEndVNode, newStartVNode, container)
             //移动DOM操作,将oldEndVNode.el移动到oldStartVNode.el的前面
             insert(oldEndVNode.el, container, oldStartVNode.el)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldEndVNode = oldChildren[--oldEndIdx]
             newStartVNode = newChildren[++newStartIdx]
         }
     }
    

至此,第一轮的移动操作已经完成,现在的新旧节点以及真实DOM节点的状态如下:

双端比较第一轮.jpg 这时候由于新的一组子节点的顺序与真实DOM节点顺序不一样,所以Diff算法还没有结束,还要进行下一轮更新

所以,我们需要将更新的逻辑封装到一个循环中,循环的执行条件是头部索引值要小于尾部索引值

 while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
     if(oldStartVNode.key === newStartVNode.key){
 ​
     }else if(oldEndVNode.key === newEndVNode.key){
 ​
     }else if(oldStartVNode.key === newEndVNode.key){
 ​
     }else if(oldEndVNode.key === newStartVNode.key){
         //需要调用patch函数进行打补丁
         patch(oldEndVNode, newStartVNode, container)
         //移动DOM操作,将oldEndVNode.el移动到oldStartVNode.el的前面
         insert(oldEndVNode.el, container, oldStartVNode.el)
         //移动DOM完成后,更新索引值,指向下一个位置
         oldEndVNode = oldChildren[--oldEndIdx]
         newStartVNode = newChildren[++newStartIdx]
     }
 }

做好以上工作后,第二轮比较开始,我们再来分析一下(还是按照上述的四步流程):

  • 第一步,比较旧的一组子节点中的头部节点p-1和新的一组子节点中的头部节点p-2,可以发现两者的key值不相同,所以不可复用,啥都不做

  • 第二步,比较旧的一组子节点的尾部节点p-3和新的一组子节点中的尾部节点p-3,发现两者的key值相同,可以复用

    但是,由于两者都处于尾部,所以不需要对真实DOM进行操作移动只需要打补丁就可以了

     while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
         if(oldStartVNode.key === newStartVNode.key){
     ​
         }else if(oldEndVNode.key === newEndVNode.key){
             //节点在新的顺序中仍然处于尾部,所以不需要移动,但是仍需要打补丁
             patch(oldEndVNode, newEndVNode, container)
             //更新索引和头尾部节点变量
             oldEndVNode = oldChildren[--oldEndIdx]
             newEndVNode = newChildren[--newEndIdx]
         }else if(oldStartVNode.key === newEndVNode.key){
     ​
         }else if(oldEndVNode.key === newStartVNode.key){
             //需要调用patch函数进行打补丁
             patch(oldEndVNode, newStartVNode, container)
             //移动DOM操作,将oldEndVNode.el移动到oldStartVNode.el的前面
             insert(oldEndVNode.el, container, oldStartVNode.el)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldEndVNode = oldChildren[--oldEndIdx]
             newStartVNode = newChildren[++newStartIdx]
         }
     }
    

现在,第二轮的比较已经完成,现在的新旧两组子节点与真实DOM节点的状态如下:

双端比较第二轮.jpg

接下来,要进行第三轮的比较了,还是按照上面的分析方法分析:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组子节点中的头部节点p-2,由于两者的key值不相同,所以不可复用,啥都不用干

  • 第二步,比较旧的一组子节点的尾部节点p-2和新的一组子节点中的尾部节点p-1,由于两者key值不同,所以不可复用,啥都不用干

  • 第三步,比较旧的一组子节点的头部节点p-1和新的一组子节点中的尾部节点p-1,由于两者的key值相同,可以复用

    由于这里是比较旧的一组子节点的头部节点与新的一组子节点的尾部节点,说明:节点p-1原本是头部节点,但在新的顺序中,他变成了尾部节点

    因此,我们要将节点p-1对应的真实DOM移动到旧的一组子节点尾部节点p-2所对应的真实DOM后面,同时还要更新索引到下一个位置

     while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
         if(oldStartVNode.key === newStartVNode.key){
     ​
         }else if(oldEndVNode.key === newEndVNode.key){
             //节点在新的顺序中仍然处于尾部,所以不需要移动,但是仍需要打补丁
             patch(oldEndVNode, newEndVNode, container)
             //更新索引和头尾部节点变量
             oldEndVNode = oldChildren[--oldEndIdx]
             newEndVNode = newChildren[--newEndIdx]
         }else if(oldStartVNode.key === newEndVNode.key){
             //调用patch函数打补丁
             patch(oldStartVNode, newEndVNode, container)
             //移动DOM操作,将oldStartVNode.el移动到旧的一组子节点对应的真实DOM节点后面
             insert(oldStartVNode.el, container, oldEndVNode.el.nextSibLing)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldStartVNode = oldChildren[++oldStartIdx]
             newEndVNode = newChildren[--newEndIdx]
         }else if(oldEndVNode.key === newStartVNode.key){
             //需要调用patch函数进行打补丁
             patch(oldEndVNode, newStartVNode, container)
             //移动DOM操作,将oldEndVNode.el移动到oldStartVNode.el的前面
             insert(oldEndVNode.el, container, oldStartVNode.el)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldEndVNode = oldChildren[--oldEndIdx]
             newStartVNode = newChildren[++newStartIdx]
         }
     }
    

现在,第三轮比较已经完成,现在的新旧两组子节点与真实DOM节点的状态如下:

双端比较第三轮.jpg

最后,我们发现目前的新旧两组子节点的头部索引和尾部索引发生重合,但是仍然满足循环的条件,所以还是会进行第四轮更新,分析如下:

  • 第一步,比较旧的一组子节点的头部节点p-2与新的一组子节点的头部节点p-2,发现两组key相同,可以复用,但是两者都是头部节点了,所以不需要移动只需要调用patch函数打补丁即可

     while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
         if(oldStartVNode.key === newStartVNode.key){
             //节点在新的顺序中仍然处于头部,所以不需要移动,但是仍需要打补丁
             patch(oldStartVNode, newStartVNode, container)
             //更新索引和头部节点变量
             oldStartVNode = oldChildren[++oldStartIdx]
             newStartVNode = newChildren[++newStartIdx]
         }else if(oldEndVNode.key === newEndVNode.key){
             //节点在新的顺序中仍然处于尾部,所以不需要移动,但是仍需要打补丁
             patch(oldEndVNode, newEndVNode, container)
             //更新索引和尾部节点变量
             oldEndVNode = oldChildren[--oldEndIdx]
             newEndVNode = newChildren[--newEndIdx]
         }else if(oldStartVNode.key === newEndVNode.key){
             //调用patch函数打补丁
             patch(oldStartVNode, newEndVNode, container)
             //移动DOM操作,将oldStartVNode.el移动到旧的一组子节点对应的真实DOM节点后面
             insert(oldStartVNode.el, container, oldEndVNode.el.nextSibLing)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldStartVNode = oldChildren[++oldStartIdx]
             newEndVNode = newChildren[--newEndIdx]
         }else if(oldEndVNode.key === newStartVNode.key){
             //需要调用patch函数进行打补丁
             patch(oldEndVNode, newStartVNode, container)
             //移动DOM操作,将oldEndVNode.el移动到oldStartVNode.el的前面
             insert(oldEndVNode.el, container, oldStartVNode.el)
             //移动DOM完成后,更新索引值,指向下一个位置
             oldEndVNode = oldChildren[--oldEndIdx]
             newStartVNode = newChildren[++newStartIdx]
         }
     }
    

现在,第四轮的比较也已经完成,现在的新旧两组子节点与真实DOM节点的状态如下:

双端比较第四轮.jpg

到这里,索引的值已经不满足循环的条件了,所以循环终止,双端Diff算法执行完毕

双端比较的优势

在上一节,我们已经了解了双端比较的原理,接下来,我们来比较一下双端Diff和简单Diff

我们使用之前的例子:

新旧节点顺序改变.jpg

在这个例子中,如果使用简单Diff算法,我们需要两次移动,即将节点p-1和节点p-2移动到p-3的后面:

移动节点图示.jpg

现在我们使用双端Diff算法来对此例进行更新,分析如下:

首先进行第一轮比较,按照上一节解析的4步流程:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组子节点的头部节点p-3,两者key值不同,不能复用,啥都不干

  • 第二步,比较旧的一组子节点中的尾部节点p-3与新的一组子节点的尾部节点p-2,两者key值不同,不能复用,啥都不干

  • 第三步,比较旧的一组子节点中的头部节点p-1与新的一组子节点的尾部节点p-2,两者key值不同,不能复用,啥都不干

  • 第四步,比较旧的一组子节点中的尾部节点p-3与新的一组子节点的头部节点p-3,两者key值相同,可以进行复用

    现在该结点原本处于所有子节点的尾部,现在在新的一组子节点中它处于头部,因此只需要让节点p-3的真实DOM作为新的头部节点即可

    移动完成之后,现在新旧两组子节点以及真实DOM节点的状态如图:

    双端优势第一轮.jpg

在这一轮比较过后,真实DOM节点的顺序已经与新的一组子节点的顺序一致了,但是算法还是会继续执行的

开始第二轮比较(图省略):

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组子节点的头部节点p-1,两者key值相同,可以进行复用

    但是由于两者都处于头部,所以不需要进行移动,只需要打补丁就可以了

至此,第二轮比较也已经完成,开始第三轮比较(图省略):

  • 第一步,比较旧的一组子节点中的头部节点p-2与新的一组子节点的头部节点p-2,两者key值相同,可以进行复用

    但是由于两者都处于头部,所以不需要进行移动,只需要打补丁就可以了

现在,索引的值已经不满足循环要求了,所以更新结束

可以看到,对于同样的例子,双端Diff算法只需要一次移动操作就可以完成更新,而简单Diff算法需要两次

非理想状况的处理方式

我们可以发现,之前的例子都比较理想,双端Diff算法分为四个步骤,而之前的例子每一轮都会命中四个步骤之一,这是非常理想的情况,实际上,并非所有情况都那么理想

现在,我们来看一个新的例子:

非理想情况下双端例子.jpg

我们按照之前的算法思路再来进行分析:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组子节点的头部节点p-2,两者key值不同,不能复用,啥都不干
  • 第二步,比较旧的一组子节点中的尾部节点p-4与新的一组子节点的尾部节点p-3,两者key值不同,不能复用,啥都不干
  • 第三步,比较旧的一组子节点中的头部节点p-1与新的一组子节点的尾部节点p-3,两者key值不同,不能复用,啥都不干
  • 第四步,比较旧的一组子节点中的尾部节点p-4与新的一组子节点的头部节点p-2,两者key值不同,不能复用,啥都不干

在这里,我们可以看到四个步骤比较过程中,都不能找到可复用的节点,所以我们只能增加额外的处理步骤

处理思路:既然两端四个端点都没有可复用的节点,那么我们就尝试一下非头部、非尾部的节点能否复用

具体做法:拿新的一组子节点中的头部节点去旧的一组子节点中寻找,找到的话说明他在更新后一定会变成头部节点

  • 所以这里应该进行一轮比较,拿新的一组子节点的头部节点p-2去旧的一组子节点中查找,会在索引1的位置找到可复用节点,所以也就意味着,节点p-2原来不是头部节点,更新后会变成头部节点

     while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
         if(oldStartVNode.key === newStartVNode.key){
             //省略部分代码
         }else if(oldEndVNode.key === newEndVNode.key){
             //省略部分代码
         }else if(oldStartVNode.key === newEndVNode.key){
             //省略部分代码
         }else if(oldEndVNode.key === newStartVNode.key){
             //省略部分代码
         }else{
             //遍历旧的一组子节点,试图寻找与newStartVNode拥有相同key值得节点
             //idxInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
             const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
             //如果idxInOld大于0,说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
             if(idxInOld > 0){
                 //idxInOld位置对应的vnode就是需要移动的节点
                 const vnodeToMove = oldChildren[idxInOld]
                 // 不要忘记移动操作还需要打补丁
                 patch(vnodeToMove, newStartVNode, container)
                 //将要移动的元素的真实DOM移动到头部节点oldStartVNode的真实DOM之前,因此使用后者作为锚点
                 insert(vnodeToMove.el, container, oldStartVNode.el)
                 //由于该位置的节点的真实DOM已经移动到别处了,所以要将其设置为undefined
                 oldChildren[idxInOld] = undefined
                 //最后更新newStartIdx到下一个位置
                 newStartVNode = newChildren[++newStartIdx]
             }
         }
     }
    

现在经过上述的更新过程,新旧两组子节点以及真实DOM节点的状态如图:

非理想情况下双端第一轮.jpg

接下来,继续进行上述的四步流程,可以发现,可以找到可复用的节点,此处不详细描述四步流程的分析过程,在最后一步的时候,可以发现旧的一组子节点的末尾节点p-4和新的一组子节点的头部节点p-4key值一样,进行我们之前的更新移动过程就可以了

此时,真实DOM节点的顺序是:p-2p-4p-1p-3

接着进行新一轮的比较,按照之前的四步流程,可以发现能够找到复用的节点,旧的一组子节点的头部节点p-1和新的一组子节点的头部节点p-1key值一样,进行我们之前的更新打补丁过程就可以了

此时,新旧两组子节点与真实DOM节点的状态如图所示:

非理想情况下双端最后一轮.jpg

最后一轮,可以发现,现在oldStartIdx指针已经指向undefined了,说明该结点已经被处理过了,因此不需要处理它,直接跳过即可,代码逻辑:

 while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
     if(!oldStartVNode){
         //如果旧节点的头部节点为undefined,则说明该结点被处理过了,直接跳到下一个位置
         oldStartVNode = oldChildren[++oldStartIdx]
     }else if(!oldEndVNode){
         //如果旧节点的尾部节点为undefined,则说明该结点被处理过了,直接跳到上一个位置
         oldEndVNode = oldChildren[--oldEndIdx]
     }else if(oldStartVNode.key === newStartVNode.key){
         //省略部分代码
     }else if(oldEndVNode.key === newEndVNode.key){
         //省略部分代码
     }else if(oldStartVNode.key === newEndVNode.key){
         //省略部分代码
     }else if(oldEndVNode.key === newStartVNode.key){
         //省略部分代码
     }else{
         const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
         if(idxInOld > 0){
             const vnodeToMove = oldChildren[idxInOld]
             patch(vnodeToMove, newStartVNode, container)
             insert(vnodeToMove.el, container, oldStartVNode.el)
             oldChildren[idxInOld] = undefined
             newStartVNode = newChildren[++newStartIdx]
         }
     }
 }

跳过之后,四个指针就两两重合了,所以按照我们之前的步骤,直接再进行比较就可以了

  • 旧的一组子节点的头部节点p-3和新的一组子节点的头部节点p-3key值一样,由于都是头部节点,所以进行我们之前的更新打补丁过程就可以了

最终,我们的真实DOM节点的顺序与新的一组子节点的顺序一致, 都是p-2p-4p-1p-3

添加新元素

我们已经考虑完非理想状态了,接下来,就剩下两个特殊情况了,就是添加新元素和移除不存在的元素

这一节中,我们先着重了解一下添加新元素的实现

同样来看一个例子:

双端Diff添加新元素例子.jpg

首先,我们先尝试进行第一轮比较,会发现四个步骤都找不到可复用的节点,于是我们尝试拿新的一组子节点中的头部节点p-4去旧的一组子节点中寻找具有相同key值的节点,但是在旧的一组子节点中也找不到p-4节点

这就说明,p-4节点是一个新增节点,我们应该将其挂载到正确的位置,那这个位置怎么确定呢?

只要将他挂载到当前的头部节点即可,因为p-4节点是新的一组子节点中的头部节点

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
    if(!oldStartVNode){
        oldStartVNode = oldChildren[++oldStartIdx]
    }else if(!oldEndVNode){
        oldEndVNode = oldChildren[--oldEndIdx]
    }else if(oldStartVNode.key === newStartVNode.key){
        //省略部分代码
    }else if(oldEndVNode.key === newEndVNode.key){
        //省略部分代码
    }else if(oldStartVNode.key === newEndVNode.key){
        //省略部分代码
    }else if(oldEndVNode.key === newStartVNode.key){
        //省略部分代码
    }else{
        const idxInOld = oldChildren.findIndex(node => node.key === newStartVNode.key)
        if(idxInOld > 0){
            const vnodeToMove = oldChildren[idxInOld]
            patch(vnodeToMove, newStartVNode, container)
            insert(vnodeToMove.el, container, oldStartVNode.el)
            oldChildren[idxInOld] = undefined
        }else{
            //将newStartVNode作为新节点挂载到头部,使用当前头部oldStartVNode.el作为锚点
            patch(null, newStartVNode, container, oldStartVNode.el)
        }
        //最后更新newStartIdx到下一个位置
        newStartVNode = newChildren[++newStartIdx]
    }
}

这样就能完成新的子节点的新增,但是这样还不是完美的,我们再来看另一个例子:

双端Diff添加新元素例子2.jpg

我们对这个例子在进行一次分析:

开始第一轮分析:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组新子节点的头部节点p-4,两者的key值不同,不可以复用
  • 第二步,比较旧的一组子节点中的尾部节点p-3与新的一组新子节点的尾部节点p-3,两者的key值相同,可以复用,找到了可复用的节点,由于都在尾部,所以进行更新打补丁

经过第一轮之后,开始第二轮:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组新子节点的头部节点p-4,两者的key值不同,不可以复用
  • 第二步,比较旧的一组子节点中的尾部节点p-2与新的一组新子节点的尾部节点p-2,两者的key值相同,可以复用,找到了可复用的节点,由于都在尾部,所以进行更新打补丁

在进行第三轮比较:

  • 第一步,比较旧的一组子节点中的头部节点p-1与新的一组新子节点的头部节点p-4,两者的key值不同,不可以复用
  • 第二步,比较旧的一组子节点中的尾部节点p-1与新的一组新子节点的尾部节点p-1,两者的key值相同,可以复用,找到了可复用的节点,由于都在尾部,所以进行更新打补丁

到现在,新子节点中只剩下p-4了,没有对应的旧子节点,但是在这时,由于遍历oldStartIdx的值大于oldEndIdx的值,满足更新停止的条件,因此更新停止,遗漏了p-4节点

双端Diff遗漏新元素.jpg 所以现在需要额外的处理代码:

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
    //省略部分代码
}
//循环结束后检查索引值的情况
if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){
    //如果满足条件,则说明有新的节点遗漏,需要挂载他们
    for(let i = newStartIdx; i <= newEndIdx; i++){
        patch(null, newChildren[i], container, oldStartVNode.el)
    }
}

移除不存在的元素

解决了新增节点后,再来看一下移除元素的情况

我们再来看一个例子:

双端Diff移除元素例子.jpg

在这个例子中,可以发现,p-2节点在新子节点中已经不存在了,为了搞清楚应该如何处理节点被移除的情况,我们还是来按照之前的四步流程分析一下:

  • 第一步,比较旧子节点中的头部节点p-1和新子节点中的头部节点p-1,两者的key值相同,所以可以复用,由于都在头部节点,所以只需要打补丁就可以了

第一轮更新已经完成,现在进行第二轮的更新:

  • 第一步,比较旧子节点中的头部节点p-2和新子节点中的头部节点p-3,两者的key值不同,所以不能复用
  • 第二步,比较旧子节点中的尾部节点p-3和新子节点中的尾部节点p-3,两者的key值相同,所以可以复用,由于都在尾部节点,所以只需要打补丁就可以了

现在,更新后新旧两组子节点以及真实DOM节点的状态如图:

双端Diff移除元素.jpg

此时newStartIdx的值大于变量newEndIdx,所以更新停止

但是现在还存在旧子节点没有被处理,应该将其移除,所以我们需要额外处理它

由图可知,索引值位于oldStartIdxoldEndIdx之间的节点应该都是需要被卸载的,所以只要写一个for循环卸载即可

while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
    //省略部分代码
}
if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx){
    for(let i = newStartIdx; i <= newEndIdx; i++){
        patch(null, newChildren[i], container, oldStartVNode.el)
    }
}else if(newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx){
    //如果存在旧节点还没有被遍历到,说明肯定是需要卸载的元素
    for(let i = oldStartIdx; i <= oldEndIdx; i++){
        unmount(oldChildren[i])
    }
}