vue diff 双端比较算法

2023-10-29

双端指针

在这里插入图片描述

  • 使用四个变量 oldStartIdx、oldEndIdx、newStartIdx 以及 newEndIdx 分别存储旧 children 和新 children 的两个端点的位置索引
let oldStartIdx = 0
let oldEndIdx = prevChildren.length - 1
let newStartIdx = 0
let newEndIdx = nextChildren.length - 1

  • 除了位置索引之外,还需要拿到四个位置索引所指向的 VNode
let oldStartVNode = prevChildren[oldStartIdx]
let oldEndVNode = prevChildren[oldEndIdx]
let newStartVNode = nextChildren[newStartIdx]
let newEndVNode = nextChildren[newEndIdx]

比较策略

  • 使用旧 children 的头一个 VNode 与新 children 的头一个 VNode 比对,即 oldStartVNode 和 newStartVNode 比较对。
  • 使用旧 children 的最后一个 VNode 与新 children 的最后一个 VNode 比对,即 oldEndVNode 和 newEndVNode 比对。
  • 使用旧 children 的头一个 VNode 与新 children 的最后一个 VNode 比对,即 oldStartVNode 和 newEndVNode 比对。
  • 使用旧 children 的最后一个 VNode 与新 children 的头一个 VNode 比对,即 oldEndVNode 和 newStartVNode 比对。
    在这里插入图片描述
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  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 比对
  }
}

命中策略四

  • 第一步:拿旧 children 中的 li-a 和新 children 中的 li-d 进行比对,由于二者 key 值不同,所以不可复用,什么都不做。
  • 第二步:拿旧 children 中的 li-d 和新 children 中的 li-c 进行比对,同样不可复用,什么都不做。
  • 第三步:拿旧 children 中的 li-a 和新 children 中的 li-c 进行比对,什么都不做。
  • 第四步:拿旧 children 中的 li-d 和新 children 中的 li-d 进行比对,由于这两个节点拥有相同的 key 值,所以我们在这次比对的过程中找到了可复用的节点。
    • li-d 节点所对应的真实 DOM 原本是最后一个子节点,并且更新之后它应该变成第一个子节点。所以我们需要把 li-d 所对应的真实 DOM 移动到最前方即可:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
	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)
	  // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
	  container.insertBefore(oldEndVNode.el, oldStartVNode.el)
	  // 更新索引,指向下一个位置
	  oldEndVNode = prevChildren[--oldEndIdx]
	  newStartVNode = nextChildren[++newStartIdx]
	}
}

命中策略二

  • 上一步更新完成之后,新的索引关系可以用下图来表示:
    在这里插入图片描述
  • 第一步:拿旧 children 中的 li-a 和新 children 中的 li-b 进行比对,由于二者 key 值不同,所以不可复用,什么都不做。
  • 第二步:拿旧 children 中的 li-c 和新 children 中的 li-c 进行比对,此时,由于二者拥有相同的 key,所以是可复用的节点,但是由于二者在新旧 children 中都是最末尾的一个节点,所以是不需要进行移动操作的,只需要调用 patch 函数更新即可,同时将相应的索引前移一位
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对

    // 调用 patch 函数更新
    patch(oldEndVNode, newEndVNode, container)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对

    // 先调用 patch 函数完成更新
    patch(oldEndVNode, newStartVNode, container)
    // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
    container.insertBefore(oldEndVNode.el, oldStartVNode.el)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newStartVNode = nextChildren[++newStartIdx]
  }
}

命中策略三

  • 上一步更新完成之后,新的索引关系可以用下图来表示:
    在这里插入图片描述
  • 第一步:拿旧 children 中的 li-a 和新 children 中的 li-b 进行比对,由于二者 key 值不同,所以不可复用,什么都不做。
  • 第二步:拿旧 children 中的 li-b 和新 children 中的 li-a 进行比对,不可复用,什么都不做。
  • 第三步:拿旧 children 中的 li-a 和新 children 中的 li-a 进行比对,此时,我们找到了可复用的节点。
    • 这一次满足的条件是:oldStartVNode.key === newEndVNode.key,这说明:li-a 节点所对应的真实 DOM 原本是第一个子节点,但现在变成了“最后”一个子节点,这里的“最后”并不是指真正意义上的最后一个节点,而是指当前索引范围内的最后一个节点。所以移动操作也是比较明显的,我们将 oldStartVNode 对应的真实 DOM 移动到 oldEndVNode 所对应真实 DOM 的后面即可
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对

    // 调用 patch 函数更新
    patch(oldEndVNode, newEndVNode, container)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newEndVNode = newEndVNode[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对

    // 调用 patch 函数更新
    patch(oldStartVNode, newEndVNode, container)
    // 将 oldStartVNode.el 移动到 oldEndVNode.el 的后面,也就是 oldEndVNode.el.nextSibling 的前面
    container.insertBefore(
      oldStartVNode.el,
      oldEndVNode.el.nextSibling
    )
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对

    // 先调用 patch 函数完成更新
    patch(oldEndVNode, newStartVNode, container)
    // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
    container.insertBefore(oldEndVNode.el, oldStartVNode.el)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newStartVNode = nextChildren[++newStartIdx]
  }
}

命中策略一

  • 上一步更新完成之后,新的索引关系可以用下图来表示:
    在这里插入图片描述
  • 第一步:拿旧 children 中的 li-b 和新 children 中的 li-b 进行比对,二者拥有相同的 key,可复用
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对

    // 调用 patch 函数更新
    patch(oldStartVNode, newStartVNode, container)
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newStartVNode = nextChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 省略...
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 省略...
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 省略...
  }
}

未命中四种策略,遍历旧节点列表

在这里插入图片描述

  • 上图中 ①、②、③、④ 这四步中的每一步比对,都无法找到可复用的节点
  • 策略为:拿新 children 中的第一个节点尝试去旧 children 中寻找,试图找到拥有相同 key 值的节点
  • 如果在旧的 children 中找到了与新 children 中第一个节点拥有相同 key 值的节点,这意味着:旧 children 中的这个节点所对应的真实 DOM 在新 children 的顺序中,已经变成了第一个节点。所以我们需要把该节点所对应的真实 DOM 移动到最前头
    在这里插入图片描述
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 {
    // 遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
    const idxInOld = prevChildren.findIndex(
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) {
      // vnodeToMove 就是在旧 children 中找到的节点,该节点所对应的真实 DOM 应该被移动到最前面
      const vnodeToMove = prevChildren[idxInOld]
      // 调用 patch 函数完成更新
      patch(vnodeToMove, newStartVNode, container)
      // 把 vnodeToMove.el 移动到最前面,即 oldStartVNode.el 的前面
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
      // 由于旧 children 中该位置的节点所对应的真实 DOM 已经被移动,所以将其设置为 undefined
      prevChildren[idxInOld] = undefined
    }
    // 将 newStartIdx 下移一位
    newStartVNode = nextChildren[++newStartIdx]
  }
}

  • 因为旧节点已经找到并处理过了,所以后续的双端比较需要跳过处理过的节点
  • 将旧 children 中的 li-b 节点变成 undefined,然后旧节点指针遇到时跳过
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // undefined 跳过
  if (!oldStartVNode) {
    oldStartVNode = prevChildren[++oldStartIdx]
  } else if (!oldEndVNode) { // undefined 跳过
    oldEndVNode = prevChildren[--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 = prevChildren.findIndex(
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) {
      const vnodeToMove = prevChildren[idxInOld]
      patch(vnodeToMove, newStartVNode, container)
      prevChildren[idxInOld] = undefined
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
    }
    newStartVNode = nextChildren[++newStartIdx]
  }
}

新增情况一

  • 节点所在的双端不满足四种策略,也不满足能找到旧节点

在这里插入图片描述

  • 在新 children 中,节点 li-d 是一个全新的节点。在这个例子中 ①、②、③、④ 这四步的比对仍然无法找到可复用节点,所以我们会尝试拿着新 children 中的 li-d 节点去旧的 children 寻找与之拥有相同 key 值的节点,结果很显然,我们无法找到这样的节点。这时说明该节点是一个全新的节点,我们应该将其挂载到容器中,由于 li-d 节点的位置索引是 newStartIdx,这说明 li-d 节点是当前这一轮比较中的“第一个”节点,所以只要把它挂载到位于 oldStartIdx 位置的节点所对应的真实 DOM 前面就可以了,即 oldStartVNode.el
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (!oldStartVNode) {
    oldStartVNode = prevChildren[++oldStartIdx]
  } else if (!oldEndVNode) {
    oldEndVNode = prevChildren[--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 = prevChildren.findIndex(
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) {
      const vnodeToMove = prevChildren[idxInOld]
      patch(vnodeToMove, newStartVNode, container)
      prevChildren[idxInOld] = undefined
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
    } else {
      // 使用 mount 函数挂载新节点,如果传入了最后一个参数,内部也是使用 insertBefore
      mount(newStartVNode, container, false, oldStartVNode.el)
    }
    newStartVNode = nextChildren[++newStartIdx]
  }
}

新增情况二

  • 节点所在的双端优先满足了四种策略

在这里插入图片描述

  • 最终双端比较完成后结果
    在这里插入图片描述
  • oldEndIdx 将比 oldStartIdx 的值要小,对 oldEndIdx 和 oldStartIdx 的值进行检查,如果在循环结束之后 oldEndIdx 的值小于 oldStartIdx 的值则说明新的 children 中存在还没有被处理的全新节点,这时我们应该调用 mount 函数将其挂载到容器元素中,观察上图可知,我们只需要把这些全新的节点添加到 oldStartIdx 索引所指向的节点之前即可
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // 省略...
}
if (oldEndIdx < oldStartIdx) {
  // 添加新节点
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    mount(nextChildren[i], container, false, oldStartVNode.el)
  }
}

删除节点

在这里插入图片描述

  • 在进行双端比较后
    在这里插入图片描述
  • 此时 newEndIdx 的值小于 newStartIdx 的值,所以循环将终止,但是通过上图可以发现,旧 children 中的 li-b 节点没有得到被处理的机会,我们应该将其移除才行,循环结束后,一旦满足条件 newEndIdx < newStartId 则说明有元素需要被移除
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // 省略...
}
if (oldEndIdx < oldStartIdx) {
  // 添加新节点
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    mount(nextChildren[i], container, false, oldStartVNode.el)
  }
} else if (newEndIdx < newStartIdx) {
  // 移除操作
  for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    container.removeChild(prevChildren[i].el)
  }
}

双端比较的优势

  • 对于如下节点情况

在这里插入图片描述

  • 如果采用 React 根据相对位置的diff 方式来对上例进行更新,则会执行两次移动操作
    • 首先会把 li-a 节点对应的真实 DOM 移动到 li-c 节点对应的真实 DOM 的后面
    • 接着再把 li-b 节点所对应的真实 DOM 移动到 li-a 节点所对应真实 DOM 的后面,即:
      在这里插入图片描述
  • 如果采用 vue 的双端比较 diff
    • 第一步:拿旧 children 中的 li-a 和新 children 中的 li-c 进行比对,由于二者 key 值不同,所以不可复用,什么都不做。
    • 第二步:拿旧 children 中的 li-c 和新 children 中的 li-b 进行比对,不可复用,什么都不做。
    • 第三步:拿旧 children 中的 li-a 和新 children 中的 li-b 进行比对,不可复用,什么都不做。
    • 第四步:拿旧 children 中的 li-c 和新 children 中的 li-c 进行比对,此时,两个节点拥有相同的 key 值,可复用。

在这里插入图片描述

  • 可以看到,我们只通过一次 DOM 移动,就使得真实 DOM 的顺序与新 children 中节点的顺序一致,后序只需要 patch 不需要移动。双端比较更加符合人类思维,在移动 DOM 方面更具有普适性,能减少因为 DOM 结构的差异而产生的影响
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue diff 双端比较算法 的相关文章

随机推荐

  • 冒泡排序 例题:给出一组数将这组数按从小到大的顺序输出出来

    冒泡排序 例题 给出一组数将这组数按从小到大的顺序输出出来 学习笔记 方便自己日后复习 也可供大家参考学习 冒泡排序百度上是这样定义的 冒泡排序 它重复的走访过要排序的元素列 依次比较两个相邻元素 如果他们的顺序 如从大到小 首字母从A到Z
  • 进程和线程的区别和联系

    一 简介 进程 进程是操作系统资源分配的基本单位 进程是指正在运行的程序实例 每个进程都有自己的内存空间 代码 数据和资源 操作系统通过管理进程来控制计算机的资源分配 每个进程都有一个唯一的标识符 称为进程 ID 以便操作系统可以识别和管理
  • NCCL error in: , unhandled system error

    今天pytorch分布式跑代码的时候出现 RuntimeError NCCL error in opt conda conda bld pytorch 1614378083779 work torch lib c10d ProcessGro
  • Vue脚手架的创建

    首先创建脚手架 初始化脚手架 Vue脚手架 是Vue官方提供的标准化开发工具 开发平台 Vue CLI 1 配置npm 2 全局安装 vue cli npm install g vue cli 3 切换到创建项目的目录 使用命令创建项目 v
  • Open3D 点云DBSCAN密度聚类并保存聚类结果

    目录 一 算法原理 1 密度聚类 2 主要函数 3 参考文献 二 代码实现 三 结果展示 1 保存聚类 2 可视化 一 算法原理 1 密度聚类 密度聚类是将簇定义为密度相连的点的最大集合 能够把具有足够高密度的区域划分为簇 并可在噪声的空间
  • 并行编程OpenCL-矩阵相加

    并行编程OpenCL 矩阵相加 1 host端代码 include
  • springboot之mybatis进阶

    springboot之mybatis进阶 简介 CRUD标签 select insert update delete resultMap sql片段 动态sql if choose when otherwise where 和set for
  • 关于使用SSM框架搭建的项目的运行方法

    目录 运行环境配置 1 安装 IDEA 开发工具 中文版设置 JDK直接下载 2 安装 MYSQL 数据库 2 1 下载安装 2 2 配置环境变量 2 4 安装 MySQL 2 4 进入 MySQL 2 5 常见问题 3 安装Tomcat
  • java日期之间的比较【项目日常】

    一 String类中提供了compareTo方法 原理是将字符串转成char 从char 0 开始进行比较 如果两值不相等 则返回相减的结果 一般将结果与0相比 进行判断 并不关心返回的具体值 String s1 2022 09 22 St
  • 蓝桥杯每日练习2

    文章目录 一 Fibonacci斐波那契数列 1 题目 2 样例 3 解析 4 Python代码 二 求圆的面积 1 题目 2 样例 3 解析 4 Python代码 三 N以内累加求和 1 题目 2 样例 3 解析 4 Python代码 四
  • 分布式文件系统 - FastDFS 在UBUNTU下安装

    分布式文件系统 FastDFS 在 CentOS 下配置安装部署 按照该博主的介绍 大部分安装操作正常 只是在创建软连接的时候报错 所以只好用笨办法启动和关闭 启动tracker usr bin fdfs trackerd etc fdfs
  • Python 实现不平衡采样

    本文将基于不平衡数据 使用Python进行反欺诈模型数据分析实战 模拟分类预测模型中因变量分类出现不平衡时该如何解决 具体的案例应用场景除反欺诈外 还有客户违约和疾病检测等 只要是因变量中各分类占比悬殊 就可对其使用一定的采样方法 以达到除
  • python命令行操作:Click包

    0 前言 在Python开发和测试过程中主要有两种模式可以选择 脚本模式 命令行模式 在代码的开发和调试过程中使用脚本模式还是很方便的 尤其接触pycharm eclipse这类强大的IDE 或者配合vs code这种综合的文本编辑器 但是
  • 【MySQL】——数据库的基本查询练习

    个人主页 努力学习的少年 版权 本文由 努力学习的少年 原创 在CSDN首发 需要转载请联系博主 如果文章对你有帮助 欢迎关注 点赞 收藏 一键三连 和订阅专栏哦 基本查询 基本查询只在一张数据表中进行查询 接下来的题目都会在下面这张表进行
  • Java多线程——线程池

    一 ThreadPoolExecutor接口 之前提到过Executors所提供的四种线程池 即 Scheduled Single Fixed Cached 如果这几种线程池不能完全满足你的需求 那么通过ThreadPoolExecutor
  • [4G&5G专题-97]:MAC层- 调度 - 上行调度的原理、过程与算法

    目录 第1章 调度概述 1 1 调度概述 1 2 无线资源调度的分类 第2章 上行调度的整体架构与过程 2 1 上行需要调度的信道 2 2 上行数据发送过程 2 3 上行调度架构 2 4 上行调度的输入信息 2 5 上行调度的步骤与过程 2
  • 【客户案例】云联壹云帮助华北电力大学搭建 AI 训练平台

    客户介绍 华北电力大学是教育部直属全国重点大学 是国家 211 工程 和 985 工程优势学科创新平台 重点建设大学 2017 年 学校进入国家 双一流 建设高校行列 重点建设能源电力科学与工程学科群 全面开启了建设世界一流学科和高水平研究
  • 如何制作属于自己的图片马

    前言 图片马是指代码写入后不破坏图片为前提 图片仍可正常打开 详细过程 自定义一个新的文件夹 文件夹里放入三个文件 一张自己喜欢的图片 自定义php代码文件 批处理文件 super png 用文本编辑器打开也没php代码
  • 深度学习基础知识

    深度学习入门者必看 25个你一定要知道的概念如果你还不了解深度学习有多么强大 不妨就从这篇文章开始 https mp weixin qq com s biz MzIzNjc1NzUzMw mid 2247485927 idx 1 sn 60
  • vue diff 双端比较算法

    文章目录 双端指针 比较策略 命中策略四 命中策略二 命中策略三 命中策略一 未命中四种策略 遍历旧节点列表 新增情况一 新增情况二 删除节点 双端比较的优势 双端指针 使用四个变量 oldStartIdx oldEndIdx newSta