图解 React diff 算法

Render 阶段会生成 Fiber Tree,所谓的 diff 实际上就是发生在这个阶段,这里的 diff 指的是 current FiberNode 和 JSX 对象之间进行对比,然后生成新的的 wip FiberNode。

除了 React 以外,其他使用到了虚拟 DOM 的前端框架也会有类似的流程,比如 Vue 里面将这个流程称之为 patch。

diff 算法本身是有性能上面的消耗,在 React 文档中有提到,即便采用最前沿的算法,如果要完整的对比两棵树,那么算法的复杂度都会达到 O(n^3),n 代表的是元素的数量,如果 n 为 1000,要执行的计算量会达到十亿量级的级别。

因此,为了降低算法的复杂度,React 为 diff 算法设置了 3 个限制:

  • 限制一:只对同级别元素进行 diff,如果一个 DOM 元素在前后两次更新中跨越了层级,那么 React 不会尝试复用它
  • 限制二:两个不同类型的元素会产生不同的树。比如元素从 div 变成了 p,那么 React 会直接销毁 div 以及子孙元素,新建 p 以及 p 对应的子孙元素
  • 限制三:开发者可以通过 key 来暗示哪些子元素能够保持稳定

更新前:

<div><p key="one">one</p><h3 key="two">two</h3>
</div>

更新后

<div><h3 key="two">two</h3><p key="one">one</p>
</div>

如果没有 key,那么 React 就会认为 div 的第一个子元素从 p 变成了 h3,第二个子元素从 h3 变成了 p,因此 React 就会采用限制二的规则。

但是如果使用了 key,那么此时的 DOM 元素是可以复用的,只不过前后交换了位置而已。

接下来我们回头再来看限制一,对同级元素进行 diff,究竟是如何进行 diff ?整个 diff 的流程可以分为两大类:

  • 更新后只有一个元素,此时就会根据 newChild 创建对应的 wip FiberNode,对应的流程就是单节点 diff
  • 更新后有多个元素,此时就会遍历 newChild 创建对应的 wip FiberNode 以及它的兄弟元素,此时对应的流程就是多节点 diff

单节点 diff

单节点指的是新节点为单一节点,但是旧节点的数量是不一定

单节点 diff 是否能够复用遵循以下的流程:

  • 步骤一 :判断 key 是否相同
  • 如果更新前后没有设置 key,那么 key 就是 null,也是属于相同的情况
  • 如果 key 相同,那么就会进入到步骤二
  • 如果 key 不同,就不需要进入步骤二,无需判断 type,结果直接为不能复用(如果有兄弟节点还会去遍历兄弟节点)
  • 步骤二 :如果 key 相同,再判断 type 是否相同
  • 如果 type 相同,那么就复用
  • 如果 type 不同,无法复用(并且兄弟节点也一并标记为删除)

更新前

<ul><li>1</li><li>2</li><li>3</li>
</ul>

更新后

<ul><p>1</p>
</ul>

这里因为没有设置 key,所以会被设为 key 是相同的,接下来就会进入到 type 的判断,此时发现 type 不同,因此不能够复用。既然这里唯一的可能性都已经不能够复用,会直接标记兄弟 FiberNode 为删除状态。

如果上面的例子中,key 不同只能代表当前的 FiberNode 无法复用,因此还需要去遍历兄弟的 FiberNode

下面我们再来看一些示例

更新前

<div>one</div>

更新后

<p>one</p>

没有设置 key,那么可以认为默认 key 就是 null,更新前后两个 key 是相同的,接下来就查看 type,发现 type 不同,因此不能复用。

更新前

<div key="one">one</div>

更新后

<div key="two">one</div>

更新前后 key 不同,不需要再判断 type,结果为不能复用

更新前

<div key="one">one</div>

更新后

<p key="two">one</p>

更新前后 key 不同,不需要再判断 type,结果为不能复用

更新前

<div key="one">one</div>

更新后

<div key="one">two</div>

首先判断 key 相同,接下来判断 type 发现也是相同,这个 FiberNode 就能够复用,children 是一个文本节点,之后将文本节点更新即可。

多节点 diff

所谓多节点 diff,指的是新节点有多个。

React 团队发现,在日常开发中,对节点的更新操作的情况往往要多余对节点“新增、删除、移动”,因此在进行多节点 diff 的时候,React 会进行两轮遍历:

  • 第一轮遍历会尝试逐个的复用节点
  • 第二轮遍历处理上一轮遍历中没有处理完的节点
第一轮遍历

第一轮遍历会从前往后依次进行遍历,存在三种情况:

  • 如果新旧子节点的key 和 type 都相同,说明可以复用
  • 如果新旧子节点的 key 相同,但是 type 不相同,这个时候就会根据 ReactElement 来生成一个全新的 fiber,旧的 fiber 被放入到 deletions 数组里面,回头统一删除。但是注意,此时遍历并不会终止
  • 如果新旧子节点的 key 和 type 都不相同,或者key不同时,导致不能复用,结束遍历

示例一

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><div key="e">e</div><div key="d">d</div>
</div>

首先遍历到 div.key.a,发现该 FiberNode 能够复用

继续往后面走,发现 div.key.b 也能够复用

接下来继续往后面走,div.key.e,这个时候发现 key 不一样,因此第一轮遍历就结束了

示例二

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><p key="c">c</p><div key="d">d</div>
</div>

首先和上面的一样,div.key.a 和 div.key.b 这两个 FiberNode 可以进行复用,接下来到了第三个节点,此时会发现 key 是相同的,但是 type 不相同,此时就会将对应的旧的 FiberNode 放入到一个叫 deletions 的数组里面,回头统一进行删除,根据新的 React 元素创建一个新的 FiberNode,但是此时的遍历是不会结束的

接下来继续往后面进行遍历,遍历什么时候结束呢?

  • 到末尾了,也就是说整个遍历完了
  • 或者是和示例一相同,可以 key 不同

第二轮遍历

如果第一轮遍历被提前终止了,那么意味着有新的 React 元素或者旧的 FiberNode 没有遍历完,此时就会采用第二轮遍历

第二轮遍历会处理这么三种情况:

  • 只剩下旧子节点:将旧的子节点添加到 deletions 数组里面之后统一删除掉(删除的情况)
  • 只剩下新的 JSX 元素:根据 ReactElement 元素来创建 FiberNode 节点(新增的情况)
  • 新旧子节点都有剩余:会将剩余的 FiberNode 节点放入一个 map 里面,遍历剩余的新的 JSX 元素,然后从 map 中去寻找能够复用的 FiberNode 节点,如果能够找到,就拿来复用。(移动的情况)
  • 如果不能找到,就新增呗。然后如果剩余的 JSX 元素都遍历完了,map 结构中还有剩余的 Fiber 节点,就将这些 Fiber 节点添加到 deletions 数组里面,之后统一做删除操作

只剩下旧子节点

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div>
</div>

遍历前面两个节点,发现能够复用,此时就会复用前面的节点,对于 React 元素来讲,遍历完前面两个就已经遍历结束了,因此剩下的FiberNode就会被放入到 deletions 数组里面,之后统一进行删除

只剩下新的 JSX 元素

更新前

<div><div key="a">a</div><div key="b">b</div>
</div>

更新后

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

根据新的 React 元素新增对应的 FiberNode 即可。

新旧子节点都有剩余(移动)

更新前

<div><div key="a">a</div><div key="b">b</div><div key="c">c</div><div key="d">d</div>
</div>

更新后

<div><div key="a">a</div><div key="c">b</div><div key="b">b</div><div key="e">e</div>
</div>

首先会将剩余的旧的 FiberNode 放入到一个 map 里面

接下来会继续去遍历剩下的 JSX 对象数组,遍历的同时,从 map 里面去找有没有能够复用

如果在 map 里面没有找到,那就会新增这个 FiberNode,如果整个 JSX 对象数组遍历完成后,map 里面还有剩余的 FiberNode,说明这些 FiberNode 是无法进行复用,直接放入到 deletions 数组里面,后期统一进行删除。

双端对比算法

所谓双端,指的是在新旧子节点的数组中,各用两个指针指向头尾的节点,在遍历的过程中,头尾两个指针同时向中间靠拢。

因此在新子节点数组中,会有两个指针,newStartIndex 和 newEndIndex 分别指向新子节点数组的头和尾。在旧子节点数组中,也会有两个指针,oldStartIndex 和 oldEndIndex 分别指向旧子节点数组的头和尾。

每遍历到一个节点,就尝试进行双端比较:「新前 vs 旧前」、「新后 vs 旧后」、「新后 vs 旧前」、「新前 vs 旧后」,如果匹配成功,更新双端的指针。比如,新旧子节点通过「新前 vs 旧后」匹配成功,那么 newStartIndex += 1,oldEndIndex -= 1。

如果新旧子节点通过「新后 vs 旧前」匹配成功,还需要将「旧前」对应的 DOM 节点插入到「旧后」对应的 DOM 节点之前。如果新旧子节点通过「新前 vs 旧后」匹配成功,还需要将「旧后」对应的 DOM 节点插入到「旧前」对应的 DOM 节点之前。

React不使用双端算法的原因

实际上在 React 的源码中,解释了为什么不使用双端 diff

function reconcileChildrenArray(
returnFiber: Fiber,currentFirstChild: Fiber | null,newChildren: Array<*>,expirationTime: ExpirationTime,
): Fiber | null {// This algorithm can't optimize by searching from boths ends since we// don't have backpointers on fibers. I'm trying to see how far we can get// with that model. If it ends up not being worth the tradeoffs, we can// add it later.// Even with a two ended optimization, we'd want to optimize for the case// where there are few changes and brute force the comparison instead of// going for the Map. It'd like to explore hitting that path first in// forward-only mode and only go for the Map once we notice that we need// lots of look ahead. This doesn't handle reversal as well as two ended// search but that's unusual. Besides, for the two ended optimization to// work on Iterables, we'd need to copy the whole set.// In this first iteration, we'll just live with hitting the bad case// (adding everything to a Map) in for every insert/move.// If you change this code, also update reconcileChildrenIterator() which// uses the same algorithm.
}

将上面的注视翻译成中文如下:

由于双端 diff 需要向前查找节点,但每个 FiberNode 节点上都没有反向指针,即前一个 FiberNode 通过 sibling 属性指向后一个 FiberNode,只能从前往后遍历,而不能反过来,因此该算法无法通过双端搜索来进行优化。

React 想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。React 认为对于列表反转和需要进行双端搜索的场景是少见的,所以在这一版的实现中,先不对 bad case 做额外的优化。

总结

diff 计算发生在更新阶段,当第一次渲染完成后,就会产生 Fiber 树,再次渲染的时候(更新),就会拿新的 JSX 对象(vdom)和旧的 FiberNode 节点进行一个对比,再决定如何来产生新的 FiberNode,它的目标是尽可能的复用已有的 Fiber 节点。这个就是 diff 算法。

在 React 中整个 diff 分为单节点 diff 和多节点 diff。

所谓单节点是指新的节点为单一节点,但是旧节点的数量是不一定的。

单节点 diff 是否能够复用遵循如下的顺序:

  1. 判断 key 是否相同
  1. 如果更新前后均未设置 key,则 key 均为 null,也属于相同的情况
  2. 如果 key 相同,进入步骤二
  3. 如果 key 不同,则无需判断 type,结果为不能复用(有兄弟节点还会去遍历兄弟节点)
  1. 如果 key 相同,再判断 type 是否相同
  1. 如果 type 相同,那么就复用
  2. 如果 type 不同,则无法复用(并且兄弟节点也一并标记为删除)

多节点 diff 会分为两轮遍历:

第一轮遍历会从前往后进行遍历,存在以下三种情况:

  • 如果新旧子节点的key 和 type 都相同,说明可以复用
  • 如果新旧子节点的 key 相同,但是 type 不相同,这个时候就会根据 ReactElement 来生成一个全新的 fiber,旧的 fiber 被放入到 deletions 数组里面,回头统一删除。但是注意,此时遍历并不会终止
  • 如果新旧子节点的 key 和 type 都不相同,结束遍历

如果第一轮遍历被提前终止了,那么意味着还有新的 JSX 元素或者旧的 FiberNode 没有被遍历,因此会采用第二轮遍历去处理。

第二轮遍历会遇到三种情况:

  • 只剩下旧子节点:将旧的子节点添加到 deletions 数组里面直接删除掉(删除的情况)
  • 只剩下新的 JSX 元素:根据 ReactElement 元素来创建 FiberNode 节点(新增的情况)
  • 新旧子节点都有剩余:会将剩余的 FiberNode 节点放入一个 map 里面,遍历剩余的新的 JSX 元素,然后从 map 中去寻找能够复用的 FiberNode 节点,如果能够找到,就拿来复用。(移动的情况)
  • 如果不能找到,就新增呗。然后如果剩余的 JSX 元素都遍历完了,map 结构中还有剩余的 Fiber 节点,就将这些 Fiber 节点添加到 deletions 数组里面,之后统一做删除操作

整个 diff 算法最最核心的就是两个字“复用”。

React 不使用双端 diff 的原因:

由于双端 diff 需要向前查找节点,但每个 FiberNode 节点上都没有反向指针,即前一个 FiberNode 通过 sibling 属性指向后一个 FiberNode,只能从前往后遍历,而不能反过来,因此该算法无法通过双端搜索来进行优化。

React 想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。React 认为对于列表反转和需要进行双端搜索的场景是少见的,所以在这一版的实现中,先不对 bad case 做额外的优化。

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

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

相关文章

Kafka之Producer原理

1. 生产者发送消息源码分析 public class SimpleProducer {public static void main(String[] args) {Properties prosnew Properties();pros.put("bootstrap.servers","192.168.8.144:9092,192.168.8.145:9092,192.168.8.146:9092"); // pros.pu…

26-LINUX--I/O复用-select

一.I/O复用概述 /O复用使得多个程序能够同时监听多个文件描述符&#xff0c;对提高程序的性能有很大帮助。以下情况适用于I/O复用技术&#xff1a; ◼ TCP 服务器同时要处理监听套接字和连接套接字。 ◼ 服务器要同时处理 TCP 请求和 UDP 请求。 ◼ 程序要同时处理多个套接…

使用 Django 构建动态网页

文章目录 创建 Django 项目和应用程序创建 HTML 模板创建视图函数配置 URL 路由运行 Django 服务器使用 Django 模板语言 Django 是一个流行的 Python Web 框架&#xff0c;它能够帮助开发人员快速构建强大的 Web 应用程序。在 Django 中&#xff0c;HTML 是用于呈现网页内容的…

1. 计算机系统概述

1. 计算机系统概述 文章目录 1. 计算机系统概述1.1 计算机的发展硬件的发展软件的发展 1.2.1 计算机硬件的基本组成早期冯诺依曼的结构现代计算机的结构 1.2.2 各个硬件的工作原理主存储器运算器控制器计算机工作过程 1.2.3 计算机系统的多级层次结构1.3 计算机的性能指标存储器…

GD32如何配置中断优先级分组以及中断优先级

使用GD32 MCU的过程中&#xff0c;大家可能会有以下疑问&#xff1a;中断优先级如何配置和使用&#xff1f; 本文将会为大家解析中断优先级分组以及中断优先级的配置使用&#xff1a; 中断优先级分组配置 一个GD32 MCU系统需要大家明确系统中使用的中断优先级分组&#xff0…

代驾公司在市场竞争中如何保持优势?

在竞争激烈的市场中&#xff0c;代驾公司可以通过多种策略保持其竞争优势&#xff0c;包括利用市场潜力、创新服务模式、提高服务效率以及加强品牌建设等。以下是具体的策略&#xff1a; 利用市场潜力 汽车产业空间巨大&#xff1a;随着汽车保有量的增加&#xff0c;代驾行业…

扫地机器人:卷价格,不如卷技术

扫地机器人内卷的终点是技术和价值&#xff0c;价格只是附属品。 一路上涨的价格&#xff0c;一路下跌的销量 从价格飙升&#xff0c;到重新卷回价格&#xff0c;尴尬的背后是扫地机器人在骨感现实下的无奈抉择。 根据数据显示&#xff0c;2020中国扫地机器人线上市场零售均价…

通过可识别性和深度学习重建大脑功能网络

摘要 本研究提出了一种新的方法来重建代表大脑动力学的功能网络&#xff0c;该方法基于两个脑区在同一认知任务中的共同参与会导致其可识别性或其动力学特性降低的观点。这种可识别性是通过深度学习模型在监督分类任务中获得的分数来估计的&#xff0c;因此不需要对这种协同参…

Flink端到端的精确一次(Exactly-Once)

目录 状态一致性 端到端的状态一致性 端到端精确一次&#xff08;End-To-End Exactly-Once&#xff09; Flink内部的Exactly-Once 输入端保证 输出端保证 幂等写入 事务写入 Flink和Kafka连接时的精确一次保证 整体介绍 需要的配置 案例 状态一致性 流式计算本身就…

无线麦克风哪个牌子性价比高?一文告诉你无线领夹麦克风怎么挑选

​当我们谈论到演讲、表演或者录制视频时&#xff0c;一个高质量的无线麦克风能够使得整个体验提升至一个全新的水平。它不仅能够保证声音的清晰度和真实度&#xff0c;还能够让使用者在演讲或者表演时更加自信和舒适。基于对市场的深入研究和用户体验的考量&#xff0c;我挑选…

【Java】解决Java报错:IllegalArgumentException

文章目录 引言1. 错误详解2. 常见的出错场景2.1 非法的参数值2.2 空值或 null 参数2.3 非法的数组索引 3. 解决方案3.1 参数验证3.2 使用自定义异常3.3 使用Java标准库中的 Objects 类 4. 预防措施4.1 编写防御性代码4.2 使用注解和检查工具4.3 单元测试 结语 引言 在Java编程…

【NPS】微软NPS配置802.1x,验证域账号,动态分配VLAN(有线网络续篇)

继上一篇文章中成功实施了有线802.1x验证域账号并动态分配VLAN的策略之后&#xff0c;我们迎来了一个新的目标&#xff1a;在用户验证失败时&#xff0c;自动分配一个Guest VLAN&#xff0c;以确保用户至少能够访问基本的网络服务。这一改进将显著提升网络的灵活性和用户的上网…

Kafka的分区副本机制

目录 生产者的分区写入策略 轮询策略 随机策略 按key分配策略 乱序分区 自定义分区策略 实现步骤&#xff1a; 消费者组Rebalance机制 Rebalance触发时机 Rebalance的不良影响 消费者分区分配策略 Range范围分配策略 RoundRobin轮询策略 Stricky粘性分配策略 生产…

计算机网络-NAT配置与ACL

目录 一、ACL 1、ACL概述 2、ACL的作用 3、ACL的分类 4、ACL的配置格式 二、NAT 1、NAT概述 2、NAT分类 2.1 、 静态NAT 2.2 、 动态NAT 3、NAT的功能 4、NAT的工作原理 三、NAT配置 1、静态NAT配置 2、动态NAT配置 四、总结 一、ACL 1、ACL概述 ACL&#xff…

让编程变得更加直观与高效 “JAVA图形化编程”官网上线!

公测预约开启 我们历经了长达三年的时光&#xff0c;执着地坚守并潜心地进行探索&#xff0c;始终怀着一颗敬畏的心&#xff0c;最终极为谨慎地推出了这款图形化编程桌面。它能够使得业务与程序清晰明了地呈现&#xff0c;而且还能与传统的低零代码平台实现紧密…

新品发布 | 飞凌嵌入式RK3576核心板,为AIoT应用赋能

为了充分满足AIoT市场对高性能、高算力和低功耗主控日益增长的需求&#xff0c;飞凌嵌入式全新推出基于Rockchip RK3576处理器开发设计的FET3576-C核心板&#xff01; 集成4个ARM Cortex-A72和4个ARM Cortex-A53高性能核&#xff0c;内置6TOPS超强算力NPU&#xff0c;为您的AI…

LeetCode 两数之和 + 三数之和

两数之和 简单题 思路&#xff1a;一个Map&#xff0c;key是数值&#xff0c;value是该数值对应的下标&#xff0c;遍历的时候判断一下当前数组下标对应的值在map里有没有可组合成target的&#xff08;具体体现为在map里找target-nums【i】)&#xff0c;如果有&#xff0c;直接…

IDEA使用阿里通义灵码插件

在这个AI火热的时代&#xff0c;纯手工写代码已经有点out了&#xff0c;使用AI插件可以帮我们快速写代码&#xff0c;起码能省去写那些简单、重复性的代码&#xff0c;大大提高编码效率&#xff0c;在这里我推荐使用阿里的通义灵码 注册安装 安装注册好后&#xff0c;打开我们…

【流媒体】音频相关概念详解

文章目录 一、前言二、概述三、音频相关概念1、采样率&#xff08;Sampling rate&#xff09;2、位深度&#xff08;Bit depth&#xff09;3、比特率&#xff08;Bit rate&#xff09;4、声道&#xff08;Audio channel&#xff09;5、音频帧6、音频编码7、音频解码 一、前言 …

搭建自己的DNS服务器

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…