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 操作的次数。
  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React中的Diff算法是通过比较新旧虚拟DOM树的差异,确定需要进行更新的部分,并尽可能地只更新这些部分,以提高性能。 Diff算法原理可以概括为以下几个步骤: 1. 树的遍历:React会逐层比较新旧虚拟DOM树的节点。从根节点开始,逐层向下比较子节点。 2. 节点比较:对于每一层的节点,React会进行以下几种比较: - 如果节点类型不同,React会直接替换整个节点及其子节点。 - 如果节点类型相同,但是属性不同,React会更新节点的属性。 - 如果节点类型相同且属性相同,React会进一步比较节点的子节点。 3. 列表节点优化:在处理列表渲染时,React会给每个列表项添加一个唯一的key属性。Diff算法会根据key来识别新增、删除或重新排序的列表项。通过key的比较,可以减少不必要的操作,提高性能。 4. 递归处理子节点:对于相同类型的节点,React会递归地比较它们的子节点。这样可以找出具体哪些子节点需要更新、添加或删除。 通过以上步骤,React可以找出需要进行更新的具体部分,并将这些变更应用到真实DOM上。这种最小化的更新方式可以减少不必要的重绘和重排,提高页面的性能和响应速度。 需要注意的是,Diff算法是一种近似算法,它会尽量找出最优的更新策略,但并不保证一定是最优解。因此,在编写React组件时,合理地设置key属性和组件结构,可以帮助Diff算法更准确地识别差异,从而提高性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值