React diff 根据相对位置的 diff 算法

文章目录

  • diff 算法
  • 没有 key 时的diff
  • 通过 key 的 diff
    • 查找需要移动的节点
    • 移动节点
    • 添加新元素
    • 移除不存在的元素
    • 缺点

diff 算法

在这里插入图片描述

没有 key 时的diff

  • 根据新旧列表的长度进行 diff
    • 公共长度相同的部分直接patch
    • 新列表长度>旧列表长度则添加,否则删除
function patchChildren(prevChildFlags,nextChildFlags,prevChildren,nextChildren,container
) {switch (prevChildFlags) {// 省略...// 旧的 children 中有多个子节点default:switch (nextChildFlags) {case ChildrenFlags.SINGLE_VNODE:// 省略...case ChildrenFlags.NO_CHILDREN:// 省略...default:// 新的 children 中有多个子节点// 获取公共长度,取新旧 children 长度较小的那一个const prevLen = prevChildren.lengthconst nextLen = nextChildren.lengthconst commonLength = prevLen > nextLen ? nextLen : prevLenfor (let i = 0; i < commonLength; i++) {patch(prevChildren[i], nextChildren[i], container)}// 如果 nextLen > prevLen,将多出来的元素添加if (nextLen > prevLen) {for (let i = commonLength; i < nextLen; i++) {mount(nextChildren[i], container)}} else if (prevLen > nextLen) {// 如果 prevLen > nextLen,将多出来的元素移除for (let i = commonLength; i < prevLen; i++) {container.removeChild(prevChildren[i].el)}}break}break}
}

通过 key 的 diff

  • 通过 key 就能够明确的知道新旧 children 中节点的映射关系,复用旧节点进行 patch
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {const nextVNode = nextChildren[i]let j = 0// 遍历旧的 childrenfor (j; j < prevChildren.length; j++) {const prevVNode = prevChildren[j]// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之if (nextVNode.key === prevVNode.key) {patch(prevVNode, nextVNode, container)break // 这里需要 break}}
}

查找需要移动的节点

在这里插入图片描述

  • 如果在寻找的过程中遇到的节点索引呈现递增趋势,则说明新旧 children 中节点顺序相同,不需要移动操作。相反的,如果在寻找的过程中遇到的索引值不呈现递增趋势,则说明需要移动操作
  • 因为 diff 是在旧真实节点列表上根据新旧虚拟 vnode 列表进行的真实移动,所以为了保证移动旧列表后的相对位置正确,很多时候都通过insertBefore 替换 appendChild
    • 取出新 children 的第一个节点,即 li-c,并尝试在旧 children 中寻找 li-c,结果是我们找到了,并且 li-c 在旧 children 中的索引为 2。
    • 取出新 children 的第二个节点,即 li-a,并尝试在旧 children 中寻找 li-a,也找到了,并且 li-a 在旧 children 中的索引为 0。
    • 递增的趋势被打破了,我们在寻找的过程中先遇到的索引值是 2,接着又遇到了比 2 小的 0,这说明在旧 children 中 li-a 的位置要比 li-c 靠前,但在新的 children 中 li-a 的位置要比 li-c 靠后。这时我们就知道了 li-a 是那个需要被移动的节点,我们接着往下执行
    • 取出新 children 的第三个节点,即 li-b,并尝试在旧 children 中寻找 li-b,同样找到了,并且 li-b 在旧 children 中的索引为 1。
    • 我们发现 1 同样小于 2,这说明在旧 children 中节点 li-b 的位置也要比 li-c 的位置靠前,但在新的 children 中 li-b 的位置要比 li-c 靠后。所以 li-b 也需要被移动。
  • 在当前寻找过程中在旧 children 中所遇到的最大索引值。如果在后续寻找的过程中发现存在索引值比最大索引值小的节点,意味着该节点需要被移动。
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {const nextVNode = nextChildren[i]let j = 0// 遍历旧的 childrenfor (j; j < prevChildren.length; j++) {const prevVNode = prevChildren[j]// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之if (nextVNode.key === prevVNode.key) {patch(prevVNode, nextVNode, container)if (j < lastIndex) {// 需要移动} else {// 更新 lastIndexlastIndex = j}break // 这里需要 break}}
}

移动节点

  • 新 children 中的第一个节点是 li-c,它在旧 children 中的索引为 2,由于 li-c 是新 children 中的第一个节点,所以它始终都是不需要移动的,只需要调用 patch 函数更新即可
function patchElement(prevVNode, nextVNode, container) {// 省略...// 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素const el = (nextVNode.el = prevVNode.el)// 省略...
}
  • 接下来是新 children 中的第二个节点 li-a,它在旧 children 中的索引是 0,由于 0 < 2 所以 li-a 是需要移动的节点,通过观察新 children 可知,新 children 中 li-a 节点的前一个节点是 li-c,所以我们的移动方案应该是:把 li-a 节点对应的真实 DOM 移动到 li-c 节点所对应真实 DOM 的后面
  • 所以我们的思路应该是想办法拿到 li-c 节点对应真实 DOM 的下一个兄弟节点,并把 li-a 节点所对应真实 DOM 插到该节点的前面
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {const nextVNode = nextChildren[i]let j = 0// 遍历旧的 childrenfor (j; j < prevChildren.length; j++) {const prevVNode = prevChildren[j]// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之if (nextVNode.key === prevVNode.key) {patch(prevVNode, nextVNode, container)if (j < lastIndex) {// 需要移动// refNode 是为了下面调用 insertBefore 函数准备的// 拿到新节点列表的上一个节点,插到其后面const refNode = nextChildren[i - 1].el.nextSibling// 调用 insertBefore 函数移动 DOMcontainer.insertBefore(prevVNode.el, refNode)} else {// 更新 lastIndexlastIndex = j}break // 这里需要 break}}
}

添加新元素

在这里插入图片描述

  • 节点 li-d 在旧的 children 中是不存在的,所以当我们尝试在旧的 children 中寻找 li-d 节点时,是找不到可复用节点的,这时就没办法通过移动节点来完成更新操作,所以我们应该使用 mount 函数将 li-d 节点作为全新的 VNode 挂载到合适的位置。
  • 查找旧节点是否存在 li-d 的 key ,不存在则新增节点
  • 如何才能保证 li-d 节点始终被添加到 li-a 节点的后面呢?答案是使用 insertBefore 方法代替 appendChild 方法,因为需要在已存在的真实节点列表进行移动,这样能够保证相对位置正确
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {const nextVNode = nextChildren[i]let j = 0,find = falsefor (j; j < prevChildren.length; j++) {const prevVNode = prevChildren[j]if (nextVNode.key === prevVNode.key) {find = truepatch(prevVNode, nextVNode, container)if (j < lastIndex) {// 需要移动const refNode = nextChildren[i - 1].el.nextSiblingcontainer.insertBefore(prevVNode.el, refNode)break} else {// 更新 lastIndexlastIndex = j}}}if (!find) {// 挂载新节点// 找到 refNodeconst refNode =i - 1 < 0? prevChildren[0].el: nextChildren[i - 1].el.nextSiblingmount(nextVNode, container, false, refNode)}
}
  • 先找到当前遍历到的节点的前一个节点,即 nextChildren[i - 1],接着找到该节点所对应真实 DOM 的下一个子节点作为 refNode,即 nextChildren[i - 1].el.nextSibling,但是由于当前遍历到的节点有可能是新 children 的第一个节点,这时 i - 1 < 0,这将导致 nextChildren[i - 1] 不存在,所以当 i - 1 < 0 时,我们就知道新的节点是作为第一个节点而存在的,这时我们只需要把新的节点插入到最前面即可,所以我们使用 prevChildren[0].el 作为 refNode
// mount 函数
function mount(vnode, container, isSVG, refNode) {const { flags } = vnodeif (flags & VNodeFlags.ELEMENT) {// 挂载普通标签mountElement(vnode, container, isSVG, refNode)}// 省略...
}// mountElement 函数
function mountElement(vnode, container, isSVG, refNode) {// 省略...refNode ? container.insertBefore(el, refNode) : container.appendChild(el)
}

移除不存在的元素

在这里插入图片描述

  • 新的 children 中已经不存在 li-c 节点了,所以我们应该想办法将 li-c 节点对应的真实 DOM 从容器元素内移除。但我们之前编写的算法还不能完成这个任务,因为外层循环遍历的是新的 children,所以外层循环会执行两次,第一次用于处理 li-a 节点,第二次用于处理 li-b 节点,此时整个算法已经运行结束了。
  • 所以,我们需要在外层循环结束之后,再优先遍历一次旧的 children,并尝试拿着旧 children 中的节点去新 children 中寻找相同的节点,如果找不到则说明该节点已经不存在于新 children 中了,这时我们应该将该节点对应的真实 DOM 移除
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {const nextVNode = nextChildren[i]let j = 0,find = falsefor (j; j < prevChildren.length; j++) {// 省略...}if (!find) {// 挂载新节点// 省略...}
}
// 移除已经不存在的节点
// 遍历旧的节点
for (let i = 0; i < prevChildren.length; i++) {const prevVNode = prevChildren[i]// 拿着旧 VNode 去新 children 中寻找相同的节点const has = nextChildren.find(nextVNode => nextVNode.key === prevVNode.key)if (!has) {// 如果没有找到相同的节点,则移除container.removeChild(prevVNode.el)}
}

缺点

在这里插入图片描述

  • 在这个例子中,我们可以通过肉眼观察从而得知最优的解决方案应该是:把 li-c 节点对应的真实 DOM 移动到最前面即可,只需要一次移动即可完成更新。然而,React 所采用的 Diff 算法在更新如上案例的时候,会进行两次移动:
    在这里插入图片描述
  • 第一次把 li-a 移动到 li-c 后面
  • 第二次把 li-b 移动到 li-a 后面

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/57113.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

九、Spring 声明式事务学习总结

文章目录 一、声明式事务1.1 什么是事务1.2 事务的应用场景1.3 事务的特性&#xff08;ACID&#xff09;1.4 未使用事务的代码示例1.5 配置 Spring 声明式事务学习总结 一、声明式事务 1.1 什么是事务 把一组业务当成一个业务来做&#xff1b;要么都成功&#xff0c;要么都失败…

ros tf

欢迎访问我的博客首页。 tf 1. tf 命令行工具1.1 发布 tf1.2 查看 tf 2.参考 1. tf 命令行工具 1.1 发布 tf 我们根据 cartographer_ros 的 launch 文件 backpack_2d.launch 写一个 tf.launch&#xff0c;并使用命令 roslaunch cartographer_ros tf.launch 启动。该 launch 文件…

了解JavaSpring

什么是Spring&#xff1f; Spring开发方向&#xff1a;分布式&#xff0c;微服务&#xff0c;网站 Spring技术&#xff08;全家桶&#xff09;&#xff1a;Spring Framework、Spring boot、Spring Cloud Spring Framework&#xff08;4.x&#xff09; 是spring体系中最基础…

powerdesigner各种字体设置;preview字体设置;sql字体设置

1.设置左侧菜单&#xff1a; 步骤如下&#xff1a; tools —> general options —> fonts —> defalut UI font ,选择字体样式及大小即可&#xff0c;同下图。 2.设置preview字体大小&#xff08;sql预览&#xff09; 步骤如下&#xff1a; tools —> general o…

选读SQL经典实例笔记19_Any和All

1. Any 1.1. 任意一个 1.2. 选修了任意一门课程的学生 1.2.1. 找出选修了至少一门课程的学生 1.3. 比任何火车都快的飞机 1.3.1. 找出比所有火车都快的飞机 2. All 2.1. 全部 2.2. 吃所有蔬菜的人 2.2.1. 没有任何一种蔬菜他们不吃 3. 问题12 3.1. 选修了全部课程的…

DARPA TC-engagement5数据集解析为json格式输出到本地

关于这个数据集的一些基本信息就不赘述了&#xff0c;参考我之前的博客。DARPA TC-engagement5数据集官方工具可视化 官方给的工具是将解析的数据存到elasticsearch的&#xff0c;但是数据集的解压增长率非常恐怖&#xff0c;对空间要求很高。因此针对这个问题&#xff0c;我对…

FreeRTOS源码分析-10 互斥信号量

目录 1 事件标志组概念及其应用 1.1 事件标志组定义 1.2 FreeRTOS事件标志组介绍 1.3 FreeRTOS事件标志组工作原理 2 事件标志组应用 2.1 功能需求 2.2 API 2.3 功能实现 3 事件标志组原理 3.1 事件标志组控制块 3.2 事件标志组获取标志位 3.3 等待事件标志触发 3.4…

如何离线安装ModHeader - Modify HTTP headers Chrome插件?

如何离线安装ModHeader - Modify HTTP headers Chrome插件&#xff1f; 1.1 前言1.2 打开Chrome浏览器的开发者模式1.3 下载并解压打包好的插件1.4 解压下载好的压缩包1.5 加载插件1.6 如何使用插件? 1.1 前言 ModHeader 是一个非常好用的Chrome浏览器插件&#xff0c;可以用…

SQL 数据科学:了解和利用联接

推荐&#xff1a;使用 NSDT场景编辑器助你快速搭建可编辑的3D应用场景 什么是 SQL 中的连接&#xff1f; SQL 联接允许您基于公共列合并来自多个数据库表的数据。这样&#xff0c;您就可以将信息合并在一起&#xff0c;并在相关数据集之间创建有意义的连接。 SQL 中的连接类型…

Java中常用的API概览及示例解析

文章目录 1. java.lang包1.1 String类1.2 StringBuilder类 2. java.util包2.1 ArrayList类2.2 HashMap类 3. java.io包3.1 File类3.2 FileInputStream和FileOutputStream类 Java作为一门广泛应用于软件开发的编程语言&#xff0c;拥有丰富的类库和API&#xff08;Application P…

PHP实践:用openssl打造安全可靠的API签名验证系统

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f3c6;本文已…

docker创建镜像并上传云端服务器

docker创建镜像并上传云端服务器 docker容器与镜像的关系1.基本镜像相关文件创建1.1 创建dockerfile文件1.2.创建do.sh文件1.3 创建upload_server_api.py文件1.4 创建upload_server_webui.py文件1.5 文件保存位置 2. 创建镜像操作2.1 创建镜像2.3 创建容器2.2 进入环境容器2.3 …