当前位置: 首页 > article >正文

React原理之Diff算法

前置文章:

  1. React原理之 React 整体架构解读
  2. React原理之整体渲染流程
  3. React原理之Fiber详解
  4. React原理之Fiber双缓冲

Diff 算法是 React 中最核心的部分,它决定了 React 在更新时如何高效地复用和更新 FiberNode。

前面我们提到:

在构建 workInProgress Fiber Tree 时会尝试复用 current Fiber Tree 中对应的 FiberNode 的数据,这个决定是否复用的过程就是 Diff 算法。

除了 workInProgress Fiber Treecurrent Fiber Tree 的构建关系,我们还需要了解一个概念:JSX,即类组件的 render 方法的返回结果,或函数组件的调用结果。JSX 对象中包含描述 DOM 节点的信息。

Diff 算法的本质就是对比 current Fiber Tree 和 JSX 对象,生成 workInProgress Fiber Tree

当组件的状态或者属性发生变化时,React 需要决定如何更新 DOM 来反映这些变化。Diff 算法就是用来决定哪些部分的 DOM 需要更新的算法

Diff 算法的特点

Diff 算法具有以下特点:

  1. 分层,同级比较:React 将整个 DOM 树分为多个层级,然后逐层比较,只比较同一层级的节点,从而减少比较的复杂度。同级比较时按照从左到右的顺序进行比较。

  2. 元素类型对比: 两个不同类型的元素会生成不同的树,如果元素类型发生了变化,React 会销毁旧树并创建新树。

  3. key 属性:React 使用 key 属性来标识节点的唯一性,从而在比较时能够快速定位到需要更新的节点。

关于 key

key 是 React 中用于标识节点的唯一性的一种机制。在 Diff 算法中,React 使用 key 属性来快速定位到需要更新的节点,从而提高 Diff 算法的性能。

我们经常强调在列表渲染中要使用 key 来提高性能,那么 key 到底是怎么帮助我们识别的呢?看一个简单的例子:

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

在上面的例子中,React 在比较两个 JSX 对象时,会按照从左到右的顺序进行比较。那么两个 JSX 在比较第一个子节点时,发现 pspan 的元素类型不同,因此会销毁旧树并创建新树。

但是由于他们有 key,React 会认为他们只是位置发生了变化,而不是元素类型发生了变化,因此会复用旧树中的节点,只是改变他们的位置。

Diff 算法的实现

Diff 算法在 React 中是通过 reconcileChildFibers 函数实现的,该函数会根据 current Fiber NodeJSX 对象 生成 workInProgress Fiber Node

reconcileChildFibers 函数中,React 会根据 current Fiber Node 和 JSX 对象的类型进行不同的处理:

  1. 当 current Fiber Node 和 JSX 对象的类型相同时,React 会递归地调用 reconcileChildFibers 函数来比较子节点,并生成对应的 workInProgress Fiber Node。如果子节点类型不同,React 会销毁旧树并创建新树。
  2. 当 current Fiber Node 和 JSX 对象的类型不同时,React 会销毁旧树并创建新树。

在比较子节点时,React 会使用 key 属性来标识节点的唯一性,从而快速定位到需要更新的节点。

看一下源码片段:

function reconcileChildFibers(returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any): Fiber | null {
	// ...

	// 处理对象类型的新子元素
	if (typeof newChild === 'object' && newChild !== null) {
		switch (newChild.$$typeof) {
			case REACT_ELEMENT_TYPE:
				// 处理单一的 React 元素
				return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
			case REACT_PORTAL_TYPE:
				// 处理 React portal
				return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes));
			case REACT_LAZY_TYPE:
				// 处理懒加载的组件
				const payload = newChild._payload;
				const init = newChild._init;

				return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes);
		}

		// 如果新子元素是一个数组,协调数组中的每个子元素。
		if (isArray(newChild)) {
			return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
		}
		// 如果新子元素是一个可迭代对象,协调迭代器中的每个子元素。
		if (getIteratorFn(newChild)) {
			return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes);
		}
		// 如果新子元素是一个可迭代对象,协调迭代器中的每个子元素。
		throwOnInvalidObjectType(returnFiber, newChild);
	}

	// 如果新子元素是一个非空字符串或数字,协调单个文本节点。
	if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') {
		return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes));
	}

	//...

	// 如果新子元素是 null 或 undefined,删除当前的所有子节点。
	return deleteRemainingChildren(returnFiber, currentFirstChild);
}
// ...

Diff 的流程

React 的 diff 算法分为两轮遍历:

第一轮遍历,处理可复用的节点。

第二轮遍历,遍历第一轮剩下的 fiber。

第一轮遍历

第一轮遍历的三种情况:

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

示例:

更新前
<ul>
	<li key="a">a</li>
	<li key="b">b</li>
	<li key="c">c</li>
	<li key="d">d</li>
</ul>
更新后
<ul>
	<li key="a">a</li>
	<li key="b">b</li>
	<li key="c2">c2</li>
	<li key="d">d</li>
</ul>

以上结构,经过前面 Fiber 的学习,我们可以知道结构是这样的:
在这里插入图片描述

为了方便我们看同级的比较,ul部分我们暂时省略。
经过前面对 fiber 双缓冲的学习,我们知道目前可以看到的这些是 current fiber,而我们要通过对比创建workInProgress Fiber。下面就是对比的过程:

在这里插入图片描述

遍历到第一个 li 时,发现 key 相同,type 相同,可以复用。

关于 alternate,是用来关联 wip Fiber Node 和 currentFiber Node 的,可以参考前面 fiber 的学习。

在这里插入图片描述

遍历到第二个 li 时,也可以复用。
在这里插入图片描述

遍历到第三个 li 时,发现 key 不同,结束遍历。

第二轮遍历

第一轮遍历结束后,如果有节点没有遍历到,那么就会进入第二轮遍历。

还是以刚才的例子为例,第一轮遍历结束后,还剩下两个li。第二轮遍历中,首先会将剩余的旧的 FiberNode 放入到一个 map 里:
在这里插入图片描述

接下来会继续去遍历剩下的 JSX 对象数组 ,遍历的同时,从 map 里面去找有没有能够复用的。如果找到了就移动过来。如果在 map 里面没有找到,那就会新增这个 FiberNode:
在这里插入图片描述
在这里插入图片描述

如果整个 JSX 对象数组遍历完成后,map 里面还有剩余的 FiberNode,说明这些 FiberNode 是无法进行复用,就将这些 Fiber 节点添加到 deletions 数组 里面,之后统一删除。

第二个示例

前面例子比较简单,可以对照以上流程再看一个示例。
更新前:

<ul>
	<li key="a">a</li>
	<li key="b">b</li>
	<li key="c">c</li>
	<li key="d">d</li>
	<li key="e">e</li>
</ul>

更新后:

<ul>
	<li key="a">a</li>
	<li key="b">b</li>
	<li key="e">e</li>
	<li key="f">f</li>
	<li key="c">c</li>
</ul>

第一轮遍历和前面示例一样,第一个 li:a 和第二个 li: b 的 key 和 type 都相同,可以复用。遍历到第三个 li 时,发现 key 不同,结束遍历。
在这里插入图片描述

第二轮遍历:

剩余的旧的 FiberNode 放入到一个 map 里:
在这里插入图片描述

继续遍历,从 map 里面去找有 key 为 e, type 为 li 的,找到了,移过来复用:
在这里插入图片描述

map 中没有 li.f,新增:
在这里插入图片描述

map 中有 li.c,复用:
在这里插入图片描述

JSX 数组遍历完成,map 中还剩下 li.d:
在这里插入图片描述

这个 FiberNode 无法复用,添加到 deletions 数组中,之后删除。

Diff 算法的优化

为了提高 Diff 算法的性能,React 在实现时做了一些优化:

  1. 避免不必要的比较:React 在比较同级节点时,会按照从左到右的顺序进行比较,从而避免出现跨层级的节点移动问题。
  2. 使用 key 属性:React 使用 key 属性来标识节点的唯一性,从而在比较时能够快速定位到需要更新的节点。
  3. 批量更新:React 在更新 DOM 时,会将多个更新操作合并为一个,从而减少 DOM 操作的次数。

http://www.kler.cn/a/284882.html

相关文章:

  • 【循环神经网络】
  • go语言 分布式一致
  • ROM修改进阶教程------安卓14 安卓15去除app签名验证的几种操作步骤 详细图文解析
  • SpringCloud框架学习(第二部分:Consul、LoadBalancer和openFeign)
  • 【论文复现】ChatGPT多模态命名实体识别
  • 面试:TCP、UDP如何解决丢包问题
  • ESP32-S3 IDF框架 控制 WS2812 灯条:实现多种灯效
  • 【通过h5作为中转页跳转到微信小程序】
  • 【Linux】06.Linux 下的编译器——gcc/g++
  • 【C++ 第十八章】C++11 新增语法(2)
  • vue3+el-tale封装(编辑、删除、查看详情按钮一起封装)
  • 【HarmonyOS 4.0】@ohos.router 页面路由
  • ★ 算法OJ题 ★ 力扣11 - 盛水最多的容器
  • sqlite3 数据插入效率
  • YOLOv8改进 | 模块缝合 | C2f融合卷积重参数化OREPA【CVPR2022】
  • Having trouble using OpenAI API
  • 回归预测|基于鹅GOOSE优化LightGBM的数据回归预测Matlab程序 多特征输入单输出 2024年优化算法
  • vue3本地运行错误集
  • 5.3 MySql实战
  • Xilinx FPGA在线升级——升级思路
  • 鸿蒙开发5.0【基于Swiper的页面布局】
  • LeetCode 热题100-9 找到字符串中所有字母异位词
  • vscode 未定义标识符 “uint16_t“C/C++(20) 但是可以顺利编译
  • Java算法—插入排序(Insertion Sort)
  • 一种导出PPT到MP4的方法
  • 大数据测试怎么做,数据应用测试、数据平台测试、数据仓库测试