Vue3 diff算法之双端diff算法详解

目录
  • 双端Diff算法
  • 双端比较的原理
    • 简单Diff的不足
    • 双端Diff介绍
  • Diff流程
    • 第一次diff
    • 第二次diff
    • 第三次diff
    • 第四次diff
  • 双端Diff的优势
  • 非理想情况的处理方式
  • 添加新元素
  • 移除不存在得节点
  • 双端Diff完整代码

双端Diff算法

双端Diff在可以解决更多简单Diff算法处理不了的场景,且比简单Diff算法性能更好。本篇是以简单Diff算法的案例来展开的,不过没有了解简单Diff算法直接看双端Diff算法也是可以看明白的。

双端比较的原理

引用简单Diff算法的例子 - 案例一

oldVNode: {
    type: 'div',
    children: [
      { type: 'p', children: '1', key: '1' },
      { type: 'p', children: '2', key: '2' },
      { type: 'p', children: '3', key: '3' },
    ]
  },
  newVNode: {
    type: 'div',
    children: [
      { type: 'p', children: '3', key: '3' },
      { type: 'p', children: '1', key: '1' },
      { type: 'p', children: '2', key: '2' }
    ]
  }

简单Diff的不足

这个案例使用简单Diff算法来更新它,会发生两次DOM移动操作。然而这并不是最优解,其实只需要将 p - 3 节点移动到 p - 1 对应的真实节点前面就可以。

这是理论上的,简单Diff算法并不能做到这一点,要想实现还得看双端Diff算法。

双端Diff介绍

顾名思义,双端Diff算法就是同时对新旧两组子节点两个端点进行比较的算法,因此需要四个索引值指向新旧虚拟节点列表的两端。

结合该案例来看双端Diff的方式 - 案例二

  newChildren: [
    { type: 'p', children: '4', key: '4' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '3', key: '3' }
  ],
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' },
    { type: 'p', children: '4', key: '4' }
  ]

newStartIdx / newEndIdx / oldStartIdx / oldEndIdx四个指针分别指向 新节点的第一个元素 / 新节点的最后一个元素 / 旧节点的第一个元素 / 旧节点的最后一个元素,指向的这四个元素称为 oldStartVNode / oldEndVNode / oldStartVNode / oldEndVNode。有了这些信息就可以互相比较了。

Diff流程

第一次diff

  • 第一步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 P-4,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,手是什么都不做。
  • 第二步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  • 第三步:比较日的一组子节点中的第一个子节点 p-1与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  • 第四步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的第一个子节点p-4。由于它们的key 值相同,因此可以进行 DOM 复用。

在第四步找到了可以复用的节点,说明它的真实DOM节点可以复用。对于可复用的节点通过移动操作即可完成更新。

那么该如何移动呢?分析下第四步比较过程的细节,第四步比较是比较旧的一组节点中的最后一个子节点 和 新的一组子节点中的第一个子节点。这说明节点 p - 4 原本是最后一个子节点,但在新的顺序中,它变成了第一个子节点。对应到代码,就是将索引 oldEndIdx 指向的虚拟节点所对应的真实DOM移动到索引 oldStartIdx 指向的虚拟节点所对应的真实DOM前面。

第一次比较完之后的情况如下:

code

第一次比较对应的代码

function insert (el, container, anchor) {
  container.insertBefore(el, anchor)
}

function patchChildren (n1, n2, container) {
  if (typeof n2.children === 'string') {
  } else if (Array.isArray(n2.children)) {
    // 到这里说明子节点都是数组类型
    patchKeyedChildren(n1, n2, container)
  }
}

function patchKeyedChildren (n1, n2, container) {
  const oldChildren = n1.children
  const newChildren = n2.children
  // 四个端点指针
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  // 四个端点元素
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]

  // 开始Diff
  if (oldStartVNode.key === newStartVNode.key) {
  //  第一步 oldStartVNode - newStartVNode
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 第二步 oldEndVNode - newEndVNode
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 第三步 oldStartVNode - newEndVNode
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 第四步 oldEndVNode - newStartVNode

    // 调用patch打补丁
    patch(oldEndVNode, newStartVNode, container)
    // 移动DOM操作    oldEndVNode.el移动 到 oldStartVNode.el 前面
    insert(oldEndVNode.el, container, oldStartVNode.el)
    // 移动DOM操作完成后
    oldEndVNode = oldChildren[--oldEndVNode]
    newStartVNode = newChildren[++newStartVNode]
  }
}

本次Diff完成之后还有其他节点需要继续进行,所以需要将diff的过程放入while循环中。满足以下情况时,说明所有节点更新完毕,因此while的条件为 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx

第二次diff

第二次diff的情况如图

  • 第一步:比较旧的一组子节点中的头部节点 p-1 与新的一组子节点中的头部节点 p-2,看看它们是否相同。由于两者的key 值不同,不可复用,所以什么都不做。(头部节点:头部素引 oldstartIdx 和 newstartIdx 所指向的节点)
  • 第二步:比较1的一组子节点中的尾部节点 p-3与新的一组子节点中的尾部节点 p-3,两者的 key 值相同,可以复用。另外,由于防者都处于尾部,因此不需要对真实 DOM进行移动操作,只需要打补丁即可。

第二次diff后新旧节点的状态如下

code

第二次比较对应代码

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode

      // 调用patch打补丁
      patch(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode

      // 调用patch打补丁
      patch(oldEndVNode, newStartVNode, container)
      // 移动DOM操作    oldEndVNode.el移动 到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el)
      // 移动DOM操作完成后
      oldEndVNode = oldChildren[--oldEndVNode]
      newStartVNode = newChildren[++newStartVNode]
    }
  }

第三次diff

第三次diff的情况如图

  • 第一步:比较旧的一组子节点中的头部节点p-1 与新的组子节点的头部节点 p-2,看看它们是否相同。由于两者key值不同 不可复用,因此什么都不做。
  • 第二步:比较旧的一组子节点中的尾部节点 p-2与新的一组子节点中的尾部节点 p-1看看它们是否相同,,由于两者key值不 同不可复用,因此什么都不做。
  • 第三步:比较旧的一组子节点中的头部节点p-1与新的一组子节申的尾部节点p-1,两者的key 值相同,可以复用。

第三步比较中找到了相同节点,说明 p - 1 原本时头部节点,但在新的顺序中,它变成了尾部节点。因此将p - 1对应的真实DOM移动到旧的子节点的尾部节点 p - 2 对应的真实DOM后面

第三次diff后的新旧节点状态如下图

code

第三次比较对应代码

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode

      // 调用patch打补丁
      patch(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode

      patch(oldStartVNode, newEndVNode, container)
      insert(oldStartVNode.el, container, newEndVNode.el.nextSibling)
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode

      // 调用patch打补丁
      patch(oldEndVNode, newStartVNode, container)
      // 移动DOM操作    oldEndVNode.el移动 到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el)
      // 移动DOM操作完成后
      oldEndVNode = oldChildren[--oldEndVNode]
      newStartVNode = newChildren[++newStartVNode]
    }
  }

第四次diff

第三次diff的情况如图

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

diff结果如图

最后while条件为假,退出diff

code

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode

      patch(oldStartVNode, newStartVNode, container)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode

      // 调用patch打补丁
      patch(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode

      patch(oldStartVNode, newEndVNode, container)
      insert(oldStartVNode.el, container, newEndVNode.el.nextSibling)
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode

      // 调用patch打补丁
      patch(oldEndVNode, newStartVNode, container)
      // 移动DOM操作    oldEndVNode.el移动 到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el)
      // 移动DOM操作完成后
      oldEndVNode = oldChildren[--oldEndVNode]
      newStartVNode = newChildren[++newStartVNode]
    }
  }

双端Diff的优势

回到最开始的例子,使用双端Diff比较的时候,第一次循环的步骤四就能找到对应 p - 3的节点,然后将其移动到p - 1之前。

只需要一次DOM操作就能完成更新。

非理想情况的处理方式

上面是一个比较理想的例子,四个步骤分别都能匹配到对应的元素,实际上并非所有情况都能匹配到。

比如 - 案例三

  newChildren: [
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '4', key: '4' },
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '3', key: '3' }
  ],
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' },
    { type: 'p', children: '4', key: '4' }
  ]

进行第一轮比较时,无法命中四个步骤中的任何一步。

  • p - 1 === p - 2
  • p - 4 === p - 3
  • p - 1 === p - 3
  • p - 4 === p - 2

因此只能通过额外的步骤来处理非理想情况。头部尾部都不能命中,那么旧看非头/尾部的节点能否命中,拿新的一组子节点的头部节点去旧的一组子节点中查找。

在旧的一组子节点中,找到与新的一组子节点头部节点具有相同key值的节点,意味着要将找到的子节点移动到真实DOM的头部。

查找结果如图

code

对应功能代码

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode
    } else {
      // 上面四个步骤都没有命中

      // 遍历旧的一组子节点,视图寻找与newStartVNode拥有相同key值的节点
      // idInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
      const idInOld = oldChildren.findIndex(
        node => node.key === newStartVNode.key
      )
      // idInOld > 0说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
      if (idInOld > 0) {
        // 找到匹配到的节点,也就是需要移动的节点
        const vNodeToMove = oldChildren[idInOld]
        // 调用 patch 更新
        patch(vNodeToMove, newStartVNode, container)
        // 将vNodeToMove插入到头部节点oldStartVNode.el之前
        insert(vNodeToMove.el, container, oldStartVNode.el)
        // 由于位置 idInOld 处的节点对应的真实DOM已经发生移动,因此将其设置为 undefined
        oldChildren[idInOld] = undefined
        // 最后更新 newStartVNode 到下一个位置
        newStartVNode = newChildren[++newStartVNode]
      }

    }
  }

接下来第二个和第三个新节点都可以通过双端对比找到匹配的节点,过程与 第二个案例 同理。

直到最后一个节点,此时旧节点列表的头部节点是 undefined,因为该节点已经被处理过了,所以不需要再处理它,直接跳过即可。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    // 如果头部或尾部节点为undefined,说明已经处理过了,直接跳过
    if (!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode
    } else {
      }
    }
  }

接下来的情况与上个案例同理。

添加新元素

当我们拿新的一组子节点中的头部节点去旧的一组子节点中寻找可复用节点时,并不是一定能找到。

案例四

newChildren: [
    { type: 'p', children: '4', key: '4' },
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '3', key: '3' },
    { type: 'p', children: '2', key: '2' }
  ],
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' }
  ]

双端查找没有匹配到

循环查找也没有匹配到

p - 4经过双端对比,循环查找都没有找到匹配的节点,说明 p - 4 是一个新增节点,应该将它创建并挂载到正确的位置。

那么应该挂载到什么位置呢,因为节点p - 4时新的一组子节点中的头部节点,所以只需要将它挂载到当前头部节点之前即可。头部节点指的是旧的一组子节点中的头部节点对应的真实DOM节点 p - 1。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode
    } else {
      // 上面四个步骤都没有命中

      // 遍历旧的一组子节点,视图寻找与newStartVNode拥有相同key值的节点
      // idInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
      const idInOld = oldChildren.findIndex(
        node => node.key === newStartVNode.key
      )
      // idInOld > 0说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
      if (idInOld > 0) {
        // 找到匹配到的节点,也就是需要移动的节点
        const vNodeToMove = oldChildren[idInOld]
        // 调用 patch 更新
        patch(vNodeToMove, newStartVNode, container)
        // 将vNodeToMove插入到头部节点oldStartVNode.el之前
        insert(vNodeToMove.el, container, oldStartVNode.el)
        // 由于位置 idInOld 处的节点对应的真实DOM已经发生移动,因此将其设置为 undefined
        oldChildren[idInOld] = undefined
        // 最后更新 newStartVNode 到下一个位置
        newStartVNode = newChildren[++newStartVNode]
      } else {
        // 将 newStartVNode作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
        patch(null, newStartVNode, container, oldStartVNode.el)
      }
      // 更新头部节点
      newStartVNode = newChildren[++newStartIdx]
    }
  }

当条件 idInOld > 0不成立时,说明没有可以复用的节点,又由于newStartVNode是头部节点,因此应该将其作为新的头部节点进行挂载操作。

剩下节点的操作类似于案例二。

新增节点可能在最后一次匹配到 - 案例五

  newChildren: [
    { type: 'p', children: '4', key: '4' },
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' }
  ],
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' }
  ]

这里省去双端匹配的流程

由于变量oldStartIdx 的值大于oldEndIdx的值,满足更新停止的条件,更新停止了。但看图可知,p - 4在整个更新过程中被一楼了,没有得到任何处理。

因此可得:新的一组子节点中有一六得节点需要作为新节点挂载。索引值位于newStartIdx 和 newEndIdx 之间得所有节点都是新节点。挂载时得锚点仍然使用当前头部节点 oldStartVNode.el。

因为此时旧节点列表中得节点可能为undefined,因此可能出现问题;但新节点列表得顺序已经被更新了,所以更新过得新节点对应得真实DOM顺序都已经重新牌列,便可以取锚点:

const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

  }
  // 说明有新节点需要挂载
  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    for (let i = newStartIdx; i < newEndIdx; i++) {
      const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
      patch(null, newChildren[i], container, anchor)
    }
  }

移除不存在得节点

除了新增节点得问题后,还可能存在需要移除的节点 - 案例六

  newChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '3', key: '3' }
  ],
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' }
  ]

第一次、第二次循环的步骤于案例二同理

新节点更新完之后,发现还存在未处理的旧节点,这便是需要删除的节点。

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

  }
  // 说明有新节点需要挂载
  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    for (let i = newStartIdx; i < newEndIdx; i++) {
      const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
      patch(null, newChildren[i], container, anchor)
    }
  } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
    // 有需要删除的节点
    for (let i = oldStartIdx; i < oldEndIdx; i++) {
      unmount(oldChildren[i])
    }
  }

双端Diff完整代码

function patchKeyedChildren (n1, n2, container) {
  const oldChildren = n1.children
  const newChildren = n2.children
  // 四个端点指针
  let newStartIdx = 0
  let newEndIdx = newChildren.length - 1
  let oldStartIdx = 0
  let oldEndIdx = oldChildren.length - 1
  // 四个端点元素
  let newStartVNode = newChildren[newStartIdx]
  let newEndVNode = newChildren[newEndIdx]
  let oldStartVNode = oldChildren[oldStartIdx]
  let oldEndVNode = oldChildren[oldEndIdx]

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 开始Diff
    // 如果头部或尾部节点为undefined,说明已经处理过了,直接跳过
    if (!oldStartVNode) {
      oldStartVNode = oldChildren[++oldStartIdx]
    } else if (!oldEndVNode) {
      oldEndVNode = oldChildren[--oldEndIdx]
    } else if (oldStartVNode.key === newStartVNode.key) {
      //  第一步 oldStartVNode - newStartVNode

      patch(oldStartVNode, newStartVNode, container)
      oldStartVNode = oldChildren[++oldStartIdx]
      newStartVNode = newChildren[++newStartIdx]
    } else if (oldEndVNode.key === newEndVNode.key) {
      // 第二步 oldEndVNode - newEndVNode

      // 调用patch打补丁
      patch(oldEndVNode, newEndVNode, container)
      oldEndVNode = oldChildren[--oldEndIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldStartVNode.key === newEndVNode.key) {
      // 第三步 oldStartVNode - newEndVNode

      patch(oldStartVNode, newEndVNode, container)
      insert(oldStartVNode.el, container, newEndVNode.el.nextSibling)
      oldStartVNode = oldChildren[++oldStartIdx]
      newEndVNode = newChildren[--newEndIdx]
    } else if (oldEndVNode.key === newStartVNode.key) {
      // 第四步 oldEndVNode - newStartVNode

      // 调用patch打补丁
      patch(oldEndVNode, newStartVNode, container)
      // 移动DOM操作    oldEndVNode.el移动 到 oldStartVNode.el 前面
      insert(oldEndVNode.el, container, oldStartVNode.el)
      // 移动DOM操作完成后
      oldEndVNode = oldChildren[--oldEndVNode]
      newStartVNode = newChildren[++newStartVNode]
    } else {
      // 上面四个步骤都没有命中

      // 遍历旧的一组子节点,视图寻找与newStartVNode拥有相同key值的节点
      // idInOld就是新的一组子节点的头部节点在旧的一组子节点中的索引
      const idInOld = oldChildren.findIndex(
        node => node.key === newStartVNode.key
      )
      // idInOld > 0说明找到了可复用的节点,并且需要将其对应的真实DOM移动到头部
      if (idInOld > 0) {
        // 找到匹配到的节点,也就是需要移动的节点
        const vNodeToMove = oldChildren[idInOld]
        // 调用 patch 更新
        patch(vNodeToMove, newStartVNode, container)
        // 将vNodeToMove插入到头部节点oldStartVNode.el之前
        insert(vNodeToMove.el, container, oldStartVNode.el)
        // 由于位置 idInOld 处的节点对应的真实DOM已经发生移动,因此将其设置为 undefined
        oldChildren[idInOld] = undefined
        // 最后更新 newStartVNode 到下一个位置
        newStartVNode = newChildren[++newStartVNode]
      } else {
        // 将 newStartVNode作为新节点挂载到头部,使用当前头部节点 oldStartVNode.el 作为锚点
        patch(null, newStartVNode, container, oldStartVNode.el)
      }
      // 更新头部节点
      newStartVNode = newChildren[++newStartIdx]
    }
  }

  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    // 说明有新节点需要挂载
    for (let i = newStartIdx; i < newEndIdx; i++) {
      const anchor = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
      patch(null, newChildren[i], container, anchor)
    }
  } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
    // 有需要删除的节点
    for (let i = oldStartIdx; i < oldEndIdx; i++) {
      unmount(oldChildren[i])
    }
  }
}

以上就是Vue3 diff算法之双端diff算法详解的详细内容,更多关于Vue3双端diff算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • 一文详解Vue3中简单diff算法的实现

    目录 简单Diff算法 减少DOM操作 例子 结论 实现 DOM复用与key的作用 例子 虚拟节点的key 实现 找到需要移动的元素 探索节点顺序关系 实现 如何移动元素 例子 实现 添加新元素 例子 实现 移除不存在的元素 例子 实现 总结 简单Diff算法 核心Diff只关心新旧虚拟节点都存在一组子节点的情况 减少DOM操作 例子 // 旧节点 const oldVNode = { type: 'div', children: [ { type: 'p', children: '1' },

  • 一文详解Vue 的双端 diff 算法

    目录 前言 diff 算法 简单 diff 双端 diff 总结 前言 Vue 和 React 都是基于 vdom 的前端框架,组件渲染会返回 vdom,渲染器再把 vdom 通过增删改的 api 同步到 dom. 当再次渲染时,会产生新的 vdom,渲染器会对比两棵 vdom 树,对有差异的部分通过增删改的 api 更新到 dom. 这里对比两棵 vdom 树,找到有差异的部分的算法,就叫做 diff 算法. diff 算法 我们知道,两棵树做 diff,复杂度是 O(n^3) 的,因为每个节

  • Vue的diff算法原理你真的了解吗

    目录 思维导图 0.从常见问题引入 1.生成虚拟dom 1.h方法实现 2.render方法实现 3.再次渲染 2.diff算法 1.对常见的dom做优化 情况1:末尾追加一个元素(头和头相同) 情况2:队首添加一个节点(尾和尾) 情况3:翻转类型(头和尾) 情况4:暴力比对复用 对于key的探讨 1.为什么不能没有key 2.为什么key不能是index 3.diff的遍历方式 总结 思维导图 0. 从常见问题引入 虚拟dom是什么? 如何创建虚拟dom? 虚拟dom如何渲染成真是dom? 虚

  • vue3 diff 算法示例

    目录 一.可能性(常见): 二.找规律 三.算法优化 最长的递增子序列 完整代码 一.可能性(常见): 1. 旧的:a b c新的:a b c d 2. 旧的:  a b c新的:d a b c 3. 旧的:a b c d新的:a b c 4. 旧的:d a b c新的:  a b c 5. 旧的:a b c d e i f g新的:a b e c d h f g 对应的真实虚拟节点(为方便理解,文中用字母代替): // vnode对象 const a = { type: 'div', // 标

  • vue.js diff算法原理详细解析

    目录 diff算法的概念 虚拟Dom h函数 diff对比规则 patch patchVnode updateChildren 总结 diff算法的概念 diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom.顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom. 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟

  • 详解vue的diff算法原理

    我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时,vue是怎么更新节点的? 要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们. 我们先根据真实DOM生成一颗 virtual DOM ,当

  • 简单谈谈Vue中的diff算法

    目录 概述 虚拟Dom(virtual dom) 原理 实现过程 patch方法 sameVnode函数 patchVnode函数 updateChildren函数 结语 概述 diff算法,可以说是Vue的一个比较核心的内容,之前只会用Vue来进行一些开发,具体的核心的内容其实涉猎不多,最近正好看了下这方面的内容,简单聊下Vue2.0的diff算法的实现吧,具体从几个实现的函数来进行分析 虚拟Dom(virtual dom) virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模

  • Vue3 diff算法之双端diff算法详解

    目录 双端Diff算法 双端比较的原理 简单Diff的不足 双端Diff介绍 Diff流程 第一次diff 第二次diff 第三次diff 第四次diff 双端Diff的优势 非理想情况的处理方式 添加新元素 移除不存在得节点 双端Diff完整代码 双端Diff算法 双端Diff在可以解决更多简单Diff算法处理不了的场景,且比简单Diff算法性能更好.本篇是以简单Diff算法的案例来展开的,不过没有了解简单Diff算法直接看双端Diff算法也是可以看明白的. 双端比较的原理 引用简单Diff算

  • React Diff算法不采用Vue的双端对比原因详解

    目录 前言 React 官方的解析 Fiber 的结构 Fiber 链表的生成 React 的 Diff 算法 第一轮,常见情况的比对 第二轮,不常见的情况的比对 重点如何协调更新位置信息 小结 图文解释 React Diff 算法 最简单的 Diff 场景 复杂的 Diff 场景 Vue3 的 Diff 算法 第一轮,常见情况的比对 第二轮,复杂情况的比对 Vue2 的 Diff 算法 第一轮,简单情况的比对 第二轮,不常见的情况的比对 React.Vue3.Vue2 的 Diff 算法对比

  • Vue3+TS+Vant3+Pinia(H5端)配置教程详解

    该模板将帮助您开始使用Vue 3.Vite3.0中的TypeScript以及Pinia.Vant3进行开发.该模板使用Vue3,请查看文档了解更多教程. 推荐的IDE设置 VS Code + Volar 键入支持.TS中的vue导入 因为TypeScript无法处理的类型信息.vue导入,默认情况下,它们填充为通用vue组件类型.在大多数情况下,如果您不真正关心模板之外的组件道具类型,那么这很好.然而,如果你想得到实际的道具类型.vue导入,您可以通过以下步骤启用Volar的接管模式: 1.运行

  • 深入了解Vue2中的的双端diff算法

    目录 简单diff算法 更新文本节点 key的作用 如何移动呢 双端diff算法 比较方式 非理想情况的处理方式 今天又重读了vue2的核心源码,主要之前读vue2源码的时候纯属的硬记,或者说纯粹的为了应付面试,导致我们并没有去细品源码中的精妙之处.如果回头在重读源码的时候,发现其中有很多地方是值得我们深入了解的.比如我们今天要看的“双端diff”.还记得之前就记得双端diff对比的口诀”头头.尾尾.头尾.尾头“,具体对比做了啥事,这种对比有什么好处,可以说是一无所知.今天我们就来好好的看看.

  • Python实现的数据结构与算法之双端队列详解

    本文实例讲述了Python实现的数据结构与算法之双端队列.分享给大家供大家参考.具体分析如下: 一.概述 双端队列(deque,全名double-ended queue)是一种具有队列和栈性质的线性数据结构.双端队列也拥有两端:队首(front).队尾(rear),但与队列不同的是,插入操作在两端(队首和队尾)都可以进行,删除操作也一样. 二.ADT 双端队列ADT(抽象数据类型)一般提供以下接口: ① Deque() 创建双端队列 ② addFront(item) 向队首插入项 ③ addRe

  • 详解C++图搜索算法之双端队列广搜

    目录 广度优先遍历 双端队列BFS 例题:AcWing 175. 电路维修 解题思路 AC代码 广度优先遍历 广度优先遍历是一种按照层次顺序进行访问的方法,它具有以下两种重要性质: 在访问完所有第i层的结点后,才会去访问第i+1层的结点 任意时刻,队列中至多有两个层次的结点.若其中一部分结点属于第i层,则另一部分结点属于第i+1层,并且所有第i层结点排在第i+1层之前.也就是说,广度优先遍历队列中的元素关于层次满足 双端队列BFS 在最基本的广度优先搜索中,每次沿着分支的扩展都记为“一步”,我们

  • 后端算法题解LeetCode前缀和示例详解

    目录 面试题 01.09. 字符串轮转 方法一:模拟 思路 题解 方法二:搜索子字符串 思路 题解 1480. 一维数组的动态和 方法一:前缀和 思路 题解 724. 寻找数组的中心下标 方法一:前缀和 思路 解题 面试题 01.09. 字符串轮转 面试题 01.09. 字符串轮转 难度:easy 字符串轮转.给定两个字符串 s1 和 s2,请编写代码检查 s2 是否为 s1 旋转而成(比如,waterbottle 是 erbottlewat 旋转后的字符串). 示例1: 输入:s1 = "wa

  • java 中基本算法之希尔排序的实例详解

    java 中基本算法之希尔排序的实例详解 希尔排序(Shell Sort)是插入排序的一种.也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本.希尔排序是非稳定排序算法.该方法因DL.Shell于1959年提出而得名. 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序:随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止. 基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差

  • TF-IDF算法解析与Python实现方法详解

    TF-IDF(term frequency–inverse document frequency)是一种用于信息检索(information retrieval)与文本挖掘(text mining)的常用加权技术.比较容易理解的一个应用场景是当我们手头有一些文章时,我们希望计算机能够自动地进行关键词提取.而TF-IDF就是可以帮我们完成这项任务的一种统计方法.它能够用于评估一个词语对于一个文集或一个语料库中的其中一份文档的重要程度. 在一份给定的文件里,词频 (term frequency, T

随机推荐