React原理之Diff算法

简介: 【8月更文挑战第24天】

前置文章:

React原理之 React 整体架构解读
React原理之整体渲染流程
React原理之Fiber详解
React原理之Fiber双缓冲
Diff 算法是 React 中最核心的部分,它决定了 React 在更新时如何高效地复用和更新 FiberNode。

前面我们提到:

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

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

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

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

Diff 算法的特点
Diff 算法具有以下特点:

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

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

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

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

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


a


b

1
2
3
4


b

a



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

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

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

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

当 current Fiber Node 和 JSX 对象的类型相同时,React 会递归地调用 reconcileChildFibers 函数来比较子节点,并生成对应的 workInProgress Fiber Node。如果子节点类型不同,React 会销毁旧树并创建新树。
当 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);

}
// ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Diff 的流程
React 的 diff 算法分为两轮遍历:

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

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

第一轮遍历
第一轮遍历的三种情况:

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

更新前


  • a

  • b

  • c

  • d


1
2
3
4
5
6
7
更新后


  • a

  • b

  • c2

  • d


1
2
3
4
5
6
7
以上结构,经过前面 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 数组 里面,之后统一删除。

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


  • a

  • b

  • c

  • d

  • e


1
2
3
4
5
6
7
更新后:


  • a

  • b

  • e

  • f

  • c


1
2
3
4
5
6
7
第一轮遍历和前面示例一样,第一个 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 在实现时做了一些优化:

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

相关文章
机器学习/深度学习 算法 自动驾驶
1205 0
|
6月前
|
机器学习/深度学习 算法 搜索推荐
从零开始构建图注意力网络:GAT算法原理与数值实现详解
本文详细解析了图注意力网络(GAT)的算法原理和实现过程。GAT通过引入注意力机制解决了图卷积网络(GCN)中所有邻居节点贡献相等的局限性,让模型能够自动学习不同邻居的重要性权重。
1228 0
从零开始构建图注意力网络:GAT算法原理与数值实现详解
|
7月前
|
传感器 算法 定位技术
KF,EKF,IEKF 算法的基本原理并构建推导出四轮前驱自主移动机器人的运动学模型和观测模型(Matlab代码实现)
KF,EKF,IEKF 算法的基本原理并构建推导出四轮前驱自主移动机器人的运动学模型和观测模型(Matlab代码实现)
235 2
|
7月前
|
算法
离散粒子群算法(DPSO)的原理与MATLAB实现
离散粒子群算法(DPSO)的原理与MATLAB实现
348 0
|
8月前
|
机器学习/深度学习 人工智能 编解码
AI视觉新突破:多角度理解3D世界的算法原理全解析
多视角条件扩散算法通过多张图片输入生成高质量3D模型,克服了单图建模背面细节缺失的问题。该技术模拟人类多角度观察方式,结合跨视图注意力机制与一致性损失优化,大幅提升几何精度与纹理保真度,成为AI 3D生成的重要突破。
1063 0
|
8月前
|
算法 区块链 数据安全/隐私保护
加密算法:深度解析Ed25519原理
在 Solana 开发过程中,我一直对 Ed25519 加密算法 如何生成公钥、签名以及验证签名的机制感到困惑。为了弄清这一点,我查阅了大量相关资料,终于对其流程有了更清晰的理解。在此记录实现过程,方便日后查阅。
1095 0
|
9月前
|
消息中间件 存储 缓存
zk基础—1.一致性原理和算法
本文详细介绍了分布式系统的特点、理论及一致性算法。首先分析了分布式系统的五大特点:分布性、对等性、并发性、缺乏全局时钟和故障随时发生。接着探讨了分布式系统理论,包括CAP理论(一致性、可用性、分区容错性)和BASE理论(基本可用、软状态、最终一致性)。文中还深入讲解了两阶段提交(2PC)与三阶段提交(3PC)协议,以及Paxos算法的推导过程和核心思想,强调了其在ZooKeeper中的应用。最后简述了ZAB算法,指出其通过改编的两阶段提交协议确保节点间数据一致性,并在Leader故障时快速恢复服务。这些内容为理解分布式系统的设计与实现提供了全面的基础。
|
9月前
|
存储 算法 安全
Java中的对称加密算法的原理与实现
本文详细解析了Java中三种常用对称加密算法(AES、DES、3DES)的实现原理及应用。对称加密使用相同密钥进行加解密,适合数据安全传输与存储。AES作为现代标准,支持128/192/256位密钥,安全性高;DES采用56位密钥,现已不够安全;3DES通过三重加密增强安全性,但性能较低。文章提供了各算法的具体Java代码示例,便于快速上手实现加密解密操作,帮助用户根据需求选择合适的加密方案保护数据安全。
622 58

热门文章

最新文章