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 操作的次数。

相关文章
|
1月前
|
算法 容器
令牌桶算法原理及实现,图文详解
本文介绍令牌桶算法,一种常用的限流策略,通过恒定速率放入令牌,控制高并发场景下的流量,确保系统稳定运行。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
令牌桶算法原理及实现,图文详解
|
21天前
|
存储 人工智能 缓存
【AI系统】布局转换原理与算法
数据布局转换技术通过优化内存中数据的排布,提升程序执行效率,特别是对于缓存性能的影响显著。本文介绍了数据在内存中的排布方式,包括内存对齐、大小端存储等概念,并详细探讨了张量数据在内存中的排布,如行优先与列优先排布,以及在深度学习中常见的NCHW与NHWC两种数据布局方式。这些布局方式的选择直接影响到程序的性能,尤其是在GPU和CPU上的表现。此外,还讨论了连续与非连续张量的概念及其对性能的影响。
43 3
|
26天前
|
机器学习/深度学习 人工智能 算法
探索人工智能中的强化学习:原理、算法与应用
探索人工智能中的强化学习:原理、算法与应用
|
1月前
|
负载均衡 算法 应用服务中间件
5大负载均衡算法及原理,图解易懂!
本文详细介绍负载均衡的5大核心算法:轮询、加权轮询、随机、最少连接和源地址散列,帮助你深入理解分布式架构中的关键技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5大负载均衡算法及原理,图解易懂!
|
1月前
|
缓存 算法 网络协议
OSPF的路由计算算法:原理与应用
OSPF的路由计算算法:原理与应用
47 4
|
1月前
|
存储 算法 网络协议
OSPF的SPF算法介绍:原理、实现与应用
OSPF的SPF算法介绍:原理、实现与应用
81 3
|
26天前
|
机器学习/深度学习 人工智能 算法
探索人工智能中的强化学习:原理、算法及应用
探索人工智能中的强化学习:原理、算法及应用
|
2月前
|
算法 数据库 索引
HyperLogLog算法的原理是什么
【10月更文挑战第19天】HyperLogLog算法的原理是什么
106 1
|
2月前
|
算法 JavaScript UED
Diff 算法的实现原理
【10月更文挑战第18天】Diff 算法是 Vue.js 中实现高效 DOM 更新的核心机制,通过合理的比较和优化策略,能够在保证界面正确性的同时,最大程度地减少 DOM 操作,提高应用的性能和用户体验。
39 2
|
2月前
|
算法 JavaScript
Vue 中的 Diff 算法
【10月更文挑战第18天】需要注意的是,Diff 算法虽然能够提高性能,但在某些复杂的场景下,可能仍然会存在一些性能瓶颈。因此,在实际开发中,我们需要根据具体情况合理地使用 Diff 算法,并结合其他优化手段来提高应用的性能。
20 1