双端Diff算法

双端Diff算法

双端Diff算法指的是,在新旧两组子节点的四个端点之间分别进行比较,并试图找到可复用的节点。相比简单Diff算法,双端Diff算法的优势在于,对于同样的更新场景,执行的DOM移动操作次数更少。

  1. 简单 Diff 算法(单向 Diff):
  • 工作原理:简单 Diff 算法从一个序列的起始位置开始,逐个比较元素,找出不同之处。

  • 特点:

    • 顺序性:仅从一个方向进行比较,一旦发现不同之处,就会停止继续比较。

    • 复杂度:时间复杂度为 O(n),其中 n 是序列的长度。

    • 结果不唯一:因为只按照一个方向进行比较,可能会忽略一些潜在的更优的匹配方式。

  1. 双端 Diff 算法(双向 Diff):
  • 工作原理:双端 Diff 算法同时从两个序列的起始位置开始,向中间移动,逐个比较元素,找出不同之处。
  • 特点:
    - 双向性:同时从两个方向进行比较,可以更全面地考虑匹配情况。
    - 优化效率:通过跳过相同的前缀和后缀部分,减少了比较的次数,提高了效率。
    - 复杂度:时间复杂度为 O(n+m),其中 n 和 m 分别是两个序列的长度。
    - 结果更准确:考虑了更多的匹配可能性,得到更准确的差异结果。

双端Diff算法的比较方式

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

function patchChildren(n1, n2, container) {
   
   
  if (typeof n2.children === 'string') {
   
   
    // 省略部分代码
  } else if (Array.isArray(n2.children)) {
   
   
    // 封装 patchKeyedChildren 函数处理两组子节点
    patchKeyedChildren(n1, n2, container);
  } else {
   
   
    // 省略部分代码
  }
}
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];
}

上面的代码中,我们将两组子节点的打补丁工程封装到了 patchKeyedChildren 函数中。该函数中,先获取两组新旧子节点 oldChildren 和 newChildren,然后创建四个索引值,分别指向新旧两组子节点的收尾,即 oldStartIdx(简称为旧前)、oldEndIdx(简称为旧后)、newStartIdx(简称为新前)、newEndIdx(简称为新后),以及四个索引值对应的 vnode。其中 oldStartVNode 和 oldEndVNode 是旧的一组子节点的第一个节点和最后一个节点,newStartVNode 和 newEndVNode 则是新的一组子节点的第一个子节点和最后一个子节点。

双端比较,每一轮均分为四个步骤:

  1. 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的第一个子节点p-4(简称为新前),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  2. 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  3. 比较旧的一组子节点的第一个子节点p-1(简称为旧前)于新的一组子节点的最后一个子节点p-3(简称为新后),看看他们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  4. 比较旧的一组子节点的最后一个子节点p-4(简称为旧后)于新的一组子节点的第一个子节点p-4(简称为新前)。由于他们的 key 相同,因此可以进行DOM复用。

四个步骤命中任何一步均说明命中的节点可以进行DOM复用,因此后续只需要进行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];
  while(oldEndIdx >= oldStartIdx && newEndIdx >= newStartIdx) {
   
   
    if (oldStartVNode.key === newStartVNode.key) {
   
   
      // 步骤一:oldStartVNode 和 newStartVNode 比较
      // 调用 patch 函数在 oldStartIdx 和 newStartIdx 之间打补丁
      patch(oldStartVNode, newStartVNode, container);
      // 更新相关索引到下一个位置
      oldStartVNode = oldChildren[++oldStartIdx];
      newStartVNode = newEndVNode[++newEndIdx];
    } else if(oldEndVNode.key === newEndVNode.key) {
   
   
      // 步骤二:oldEndVNode 和 newEndVNode 比较
      // 节点在新的顺序中仍然处于尾部,不需要移动,但仍需打补丁
      patch(oldEndVNode, newEndVNode, container);
      // 更新索引和头尾部节点变量
      newEndVNode = newChildren[--newEndIdx];
      oldEndVNode = oldChildren[--oldEndIdx];
    } else 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值