Vue2 Diff 算法简易版

背景

复习一下Vue2的双端比较法~

双端比较法

Vue2采用的双端比较法,即新列表和旧列表进行头尾对比,在对比的过程中指针会逐渐向内靠拢,直到某个列表的节点全部遍历过,对比停止。
举个例子:

# 旧列表
a b c d
# 新列表
d b a c

我们会先定义几个变量:

function vue2diff(prevChildren, nextChildren, parent) {
    
    
	let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
}

那么此时我们就有四个指针和四个指针对应的节点了,分别是旧列表的节点a、旧列表的节点d、新列表的节点d和新列表的节点c,为了下面方便表述:旧列表的节点a会写为oldNodeA,以此类推~
接下来说一下双端比较法的比较方法:

  1. oldStartNodenewStartNode对比key值;
  2. oldEndNode newEndNode 对比key值;
  3. oldStartNodenewEndNode 对比key值;
  4. newStartNodeoldEndNode 对比key值。
    如图所示:
    在这里插入图片描述

对比流程

接下来进入我们的对比流程~对比的过程主要是寻找拥有相同key值的节点

实现双端对比

先说一下双端对比中,如果遇到相同节点(拥有通过key)的情况:

  • oldStartNodenewStartNodekey相同时,则oldStartIndexnewStartIndex同时向后移动一位;
  • oldEndNodenewEndNodekey相同时,则oldEndIndexnewEndIndex同时向前移动一位;
  • oldStartNodenewEndNodekey相同时,则oldStartIndex向后移动一位,newEndIndex向前移动一位;
  • oldEndNodenewStartNodekey相同时,则oldEndIndex向前移动一位,newStartIndex向后移动一位。

结束循环的条件当其中一个列表的节点全部遍历完成时,则完成我们的对比过程。

按这个为例子:

# 旧列表
a b c d
# 新列表
d b a c

第一次对比的时:
在这里插入图片描述

  • oldStartNode A newStartNode D不同,继续对比;
  • oldEndNode DnewEndNode C不同,继续对比;
  • oldEndNode DnewStartNode D相同,则这时候会将旧列表中得到节点D的位置进行变化,挪到A的前面,然后oldEndIndex向前移动,newStartIndex向后移动。
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d a b c

接下来继续对比:

  • oldStartNode A newStartNode B不同,继续对比;
  • -oldEndNode CnewEndNode C相同,但是它们都属于尾节点,所以我们直接复用节点,将oldEndIndexnewEndIndex都向前移动
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d a b c

接下来继续对比:

  • oldStartNode A newStartNode B不同,继续对比;
  • oldEndNode B newEndNode A不同,继续对比;
  • oldStartNode A newEndNode A,则将旧列表中A节点挪到B节点后面即可,oldStartIndex向后移动一位,newEndIndex向前移动一位
    得到如下图:
    在这里插入图片描述
    此时虚拟DOM的结果为:
d b a c

最后就是oldEndNode B newEndNode B对比,一致,然后结束了循环~
大致代码如下:

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // ...
    }
  }
}

以上是四个头尾新旧节点对比会出现相同节点的情况,接下来我们来看一下四次对比找不到复用节点的情况:
在这里插入图片描述

  • oldStartNode AnewStartNode E 不同;
  • oldEndNode DnewEndNode H 不同;
  • oldStartNode AnewEndNode H 不同;
  • oldEndNode D 与 newStartNode E 不同;

这时候我们会先拿出新列表的第一个节点oldStartNode A,然后找到旧列表中是否存在可以复用的节点,这里就有两种情况啦,先说图上的这种:如果不存在,那么说明这个是新节点,直接放在旧列表的最前面即可;
如果是这种:
在这里插入图片描述
B的复用节点在旧列表中,那么将旧节点移动到第一个节点,并在旧列表中置成undefine
在这里插入图片描述

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  // ...
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
    
    
        mount(newStartNode, parent, oldStartNode.el)
      } else {
    
    
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
}

这时候需要考虑的问题是,当进入下一个循环的时候,对应的oldStartNode为undefined,所以代码上需要处理一下:

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
    
    
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
    
    
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
    
    
      // ...
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // ...
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // ...
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // ...
    } else {
    
    
      // ...
    }
  }
  
}

接下来我们来考虑一下列表删除的情况:
在这里插入图片描述

  • 节点A复用;
  • 节点D复用;
    此时各个坐标变化如下:
    在这里插入图片描述
// 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    
    
    while (oldStartIndex <= oldStartIndex) {
    
    
      if (!prevChildren[oldStartIndex]) {
    
    
        oldStartIndex++
        continue
      }
      // 删除节点
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } 

最后是新增节点的情况啦~
在这里插入图片描述

  • 节点A复用;
  • 节点B复用;
    此时各个坐标变化如下:
    在这里插入图片描述
if (oldStartIndex> oldEndIndex ) {
    
    
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
    
    
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }

最后贴一下完整代码:

function vue2diff(prevChildren, nextChildren, parent) {
    
    
  let
    // 旧头指针 
    oldStartIndex = 0,
    // 新头指针
    newStartIndex = 0,
    // 旧尾指针
    oldEndIndex = prevChildren.length - 1,
    // 新尾指针
    newEndIndex = nextChildren.length - 1,
    // 旧头节点
    oldStartNode = prevChildren[oldStartIndex],
    // 旧尾节点
    oldEndNode = prevChildren[oldEndIndex],
    // 新头节点
    newStartNode = nextChildren[newEndIndex],
    // 新尾节点
    newEndNode = nextChildren[newStartIndex];
  // 双端对比,当有一个列表的节点全部遍历完成,则结束循环
  while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
    
    
    // 旧列表中遇到Undefine节点则跳过对比
    if (oldStartNode === undefined) {
    
    
      oldStartNode = prevChildren[++oldStartIndex]
    } else if (oldEndNode === undefined) {
    
    
      oldEndNode = prevChildren[--oldStartIndex]
    } else if (oldStartNode.key === newStartNode.key) {
    
    
      // 当两个头节点相同时,节点不需要处理,两个指针的坐标向后移动一位
      patch(oldStartNode, newStartNode, parent)

      oldStartIndex++
      newStartIndex++
      oldStartNode = prevChildren[oldStartIndex]
      newStartNode = nextChildren[newStartIndex]
    } else if (oldEndNode.key === newEndNode.key) {
    
    
      // 当两个尾节点相同时,节点不需要处理,两个指针的坐标向前移动一位
      patch(oldEndNode, newEndNode, parent)

      oldEndIndex--
      newEndIndex--
      oldEndNode = prevChildren[oldEndIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldStartNode.key === newEndNode.key) {
    
    
      // 当旧头节点跟新尾节点相同时,需要移动旧头节点,旧指针向后移动一位,新指针向前移动一位
      patch(oldStartNode, newEndNode, parent)
      parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
      oldStartIndex++
      newEndIndex--
      oldStartNode = prevChildren[oldStartIndex]
      newEndNode = nextChildren[newEndIndex]
    } else if (oldEndNode.key === newStartNode.key) {
    
    
      // 当旧尾节点和新头节点相同时,需要移动旧头节点,旧指针向前移动一位,新指针向后移动一位
      patch(oldEndNode, newStartNode, parent)
      parent.insertBefore(oldEndNode.el, oldStartNode.el)
      oldEndIndex--
      newStartIndex++
      oldEndNode = prevChildren[oldEndIndex]
      newStartNode = nextChildren[newStartIndex]
    } else {
    
    
      // 四个节点对比的过程中没有发现相同的节点时
      // 先看看新列表的头节点是否存在于旧列表中
      let newKey = newStartNode.key,
        oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
      // 如果不存在,直接创建放在最前方即可
      if (oldIndex === -1) {
    
    
        mount(newStartNode, parent, oldStartNode.el)
      } else {
    
    
      // 如果存在,将旧节点移动到第一个节点,并在旧列表中置成undefine, 跳过对比过程
        let prevNode = prevChildren[oldIndex]
        patch(prevNode, newStartNode, parent)
        parent.insertBefore(prevNode.el, oldStartNode.el)
        prevChildren[oldIndex] = undefined
      }
      // 更新新列表的对比节点
      newStartIndex++
      newStartNode = nextChildren[newStartIndex]
    }
  }
  // 当新列表的newStartIndex 大于newEndIndex,说明新列表删除的节点
  if (newStartIndex > newEndIndex) {
    
    
    while (oldStartIndex <= oldStartIndex) {
    
    
      if (!prevChildren[oldStartIndex]) {
    
    
        oldStartIndex++
        continue
      }
      parent.removeChild(prevChildren[oldStartIndex++].el)
    }
  } else if (oldStartIndex> oldEndIndex ) {
    
    
    // 当旧列表的oldEndIndex 小于oldStartIndex,说明新列表新增了节点
    for (let i = newStartIndex; i <= newEndIndex; i++) {
    
    
      mount(nextChildren[i], parent, prevStartNode.el)
    }
  }
}

小结

重新学了一遍Vue2的diff算法:双端比较法,有了新的收获吧,思路清晰了很多。后面会写一篇Vue3的diff算法~

参考链接

猜你喜欢

转载自blog.csdn.net/qq_34086980/article/details/131552585