- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章react diff算法源码解析由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
React中Diff算法又称为调和算法,对应函数名为reconcileChildren,它的主要作用是标记更新过程中那些元素发生了变化,这些变化包括新增、移动、删除。过程发生在beginWork阶段,只有非初次渲染才会Diff.
以前看过一些文章将Diff算法表述为两颗Fiber树的比较,这是不正确的,实际的Diff过程是一组现有的Fiber节点和新的由JSX生成的ReactElement的比较,然后生成新的Fiber节点的过程,这个过程中也会尝试复用现有Fiber节点.
节点Diff又分为两种:
Element
、Portal
、string
、number
。Array
、Iterator
。以下React版本为17.0.1,代码文件为ReactChildFiber.old.js.
单节点Diff比较简单,只有key相同并且type相同的情况才会尝试复用节点,否则会返回新的节点.
单节点大部分情况下我们都不会去赋值key,所以它们默认为null,也是相同的.
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
44
45
46
47
|
// 单节点比较
function
reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber |
null
,
element: ReactElement,
lanes: Lanes,
): Fiber {
// 当前新的reactElement的key
const key = element.key;
// 当前的child fiber节点
let child = currentFirstChild;
while
(child !==
null
) {
// key相同的情况才diff
if
(child.key === key) {
switch
(child.tag) {
// ...
default
: {
// 当前fiber和reactElement的type相同时
if
(child.elementType === element.type) {
// 删除同级的其他节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用当前child fiber
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.
return
= returnFiber;
// 返回可复用的child fiber
return
existing;
}
break
;
}
}
// 不匹配删除节点
deleteRemainingChildren(returnFiber, child);
break
;
}
else
{
// key不同直接删除节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 新的Fiber节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.
return
= returnFiber;
return
created;
}
|
源码中将多节点分为了数组节点和可迭代节点.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
if
(isArray(newChild)) {
return
reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
if
(getIteratorFn(newChild)) {
return
reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
|
对应的Diff函数分别是reconcileChildrenArray和reconcileChildrenIterator。它们的核心Diff逻辑是相同的,所以只分析数组节点的Diff —— reconcileChildrenArray函数.
这一段的代码比较长,但逻辑很清晰,从分割线分为两轮遍历.
第一轮遍历只针对key和顺序都相同的情况,这些key对应的节点位置没有发生改变,只需要做更新操作,一旦遍历遇到key不同的情况就需要跳出循环.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 旧节点
<li key=
"0"
/>
<li key=
"1"
/>
<li key=
"2"
/>
// 新节点
<li key=
"0"
/>
<li key=
"1"
/>
<li key=
"5"
/>
// key="5"不同,跳出遍历
// 第一轮遍历的节点
<li key=
"0"
/>
<li key=
"1"
/>
// <li key="2"/>和<li key="5"/>留在第二轮遍历比较。
|
在第一轮遍历完后也分为两种情况.
第二轮遍历针对key不同或顺序不同的情况,可能情况如下:
1
2
3
4
5
6
7
8
9
10
|
// 旧节点
<li key=
"0"
/>
<li key=
"1"
/>
<li key=
"2"
/>
// 新节点
<li key=
"0"
/>
<li key=
"2"
/>
<li key=
"1"
/>
// 第二轮遍历对比<li key="2"/>、<li key="1"/>这两个节点
|
第二轮的遍历会稍微复杂一点,后文在细讲.
详细的代码如下.
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
function
reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber |
null
,
newChildren: Array<*>,
lanes: Lanes,
): Fiber |
null
{
// 函数返回的Fiber节点
let resultingFirstChild: Fiber |
null
=
null
;
let previousNewFiber: Fiber |
null
=
null
;
// oldFiber为链表
let oldFiber = currentFirstChild;
// 用来判断节点是否移动
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber =
null
;
// 第一轮遍历,只遍历key相同的节点
for
(; oldFiber !==
null
&& newIdx < newChildren.length; newIdx++) {
if
(oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber =
null
;
}
else
{
// 每次循环旧的fiber节点都会指向兄弟元素也就是下次循环的fiber节点
nextOldFiber = oldFiber.sibling;
}
// key相同返回fiber节点,key不同返回null
// 如果type相同复用节点,不同返回新节点
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber为null表示key不同,跳出循环
if
(newFiber ===
null
) {
if
(oldFiber ===
null
) {
oldFiber = nextOldFiber;
}
break
;
}
// newFiber.alternate为null就是新节点,说明type不同创建了新fiber节点
if
(oldFiber && newFiber.alternate ===
null
) {
// 需要把oldFiber标记删除
deleteChild(returnFiber, oldFiber);
}
// 放置节点,更新lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 组成新fiber节点链
if
(previousNewFiber ===
null
) {
resultingFirstChild = newFiber;
}
else
{
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
/*
第一轮遍历完后新节点数量少于旧节点数量
newChildren已经遍历完,删除掉剩下的fiber节点,可能情况如下 ⬇️
以前
<li key="0"/>
<li key="1"/>
<li key="2"/>
新的
<li key="0"/>
<li key="1"/>
就会把<li key="2"/>删除
*/
if
(newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return
resultingFirstChild;
}
/*
第一轮遍历完新节点数量大于旧节点数量
oldFiber已经遍历完,可能情况如下 ⬇️
以前
<li key="0"/>
<li key="1"/>
新的
<li key="0"/>
<li key="1"/>
<li key="2"/>
就会添加新的<li key="2"/>,这一段是新节点的插入逻辑
*/
if
(oldFiber ===
null
) {
for
(; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if
(newFiber ===
null
) {
continue
;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 组成新fiber节点链
if
(previousNewFiber ===
null
) {
resultingFirstChild = newFiber;
}
else
{
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return
resultingFirstChild;
}
// ---------------------------------------------------------------------
// 用剩余的oldFiber创建一个key->fiber节点的Map,方便用key来获取对应的旧fiber节点
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 第二轮遍历,继续遍历剩余的节点,这些节点可能是需要移动或者删除的
for
(; newIdx < newChildren.length; newIdx++) {
// 从map中获取对应对应key的旧节点,返回更新后的新节点
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if
(newFiber !==
null
) {
// 复用的新节点,从map里删除老的节点,对应的情况可能是位置的改变
if
(newFiber.alternate !==
null
) {
// 复用的节点要移除map,因为map里剩余的节点都会被标记Deletion删除
existingChildren.
delete
(
newFiber.key ===
null
? newIdx : newFiber.key,
);
}
// 放置节点同时节点判断是否移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if
(previousNewFiber ===
null
) {
resultingFirstChild = newFiber;
}
else
{
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除剩下的无用节点
existingChildren.forEach(child => deleteChild(returnFiber, child));
return
resultingFirstChild;
}
|
第一轮遍历比较好理解,这里再细分析一下第二轮遍历,因为第二轮会出现复用是否需要移动的问题.
第二轮遍历首先遍历剩余的oldFiber,组成一个key -> 旧fiber节点的Map,这用可以通过key来快速的获取旧节点.
接下来的遍历依然是使用的新节点为遍历对象,每次遍历使用新节点的key从Map中取出旧节点来对比是否能复用,对应的函数为updateFromMap.
如果节点存在alternate属性,则是复用的节点,这时候需要将它从existingChildren里移除,后续会把第二轮遍历完后依然存在在existingChildren里的节点标记为删除.
这里存在一个变量lastPlacedIndex用来判断节点是否移动,每次将节点添加到新的Fiber链表中,都会更新这个值.
当复用的节点oldIndex小于lastPlacedIndex时,则为移动,如果不需要移动,则会将lastPlacedIndex更新为较大的oldIndex,下一个节点会以新值判断,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
function
placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
newFiber.index = newIndex;
const current = newFiber.alternate;
if
(current !==
null
) {
const oldIndex = current.index;
if
(oldIndex < lastPlacedIndex) {
// 节点移动
newFiber.flags = Placement;
return
lastPlacedIndex;
}
else
{
// 节点位置无变化
return
oldIndex;
}
}
else
{
// 插入的新节点
newFiber.flags = Placement;
return
lastPlacedIndex;
}
}
|
举个例子:
1
2
3
4
|
// 旧
abcd
// 新
acbd
|
abcd均为key值.
第一轮遍历后剩下的需要对比节点
1
2
3
4
|
// 旧
bcd
// 新
cbd
|
a节点在第一轮已经复用,并且调用过placeChild,这时lastPlacedIndex值为0.
进入第二轮遍历,依然是以新节点为遍历对象.
1
2
3
|
c => 在旧节点中存在,可复用,它的index在旧节点中为2,2 > lastPlacedIndex(0),不需要移动,将lastPlacedIndex赋值为2。
b => 在旧节点中存在,可复用,它的index在旧节点中为1,1 < lastPlacedIndex(2),需要移动,标记Placement。
d => 在旧节点中存在,可复用,它的index在旧节点中为3,3 > lastPlacedIndex(2),不需要移动。
|
由这个例子可以看出,React中将右侧不需要移动的节点作为参照,将需要移动的节点都是统一从左向右移动的.
在后续Layout阶段会将这里标记了Placement的节点做insertBefore操作.
React中的Diff算法核心代码不算很长,但是却引入key巧妙的将复杂度由O(n3 )变为了O(n).
码农内卷太严重,所以不得不学习源码了.
以上就是react diff算法源码解析的详细内容,更多关于react diff算法的资料请关注我其它相关文章! 。
原文链接:https://juejin.cn/post/6949092569275957256 。
最后此篇关于react diff算法源码解析的文章就讲到这里了,如果你想了解更多关于react diff算法源码解析的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
ACO.Visualization项目 本项目演示蚁群算法求解旅行商问题的可视化过程,包括路径上的信息素浓度、蚁群的运动过程等。项目相关的代码:https://github.com/anycad/A
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。 关闭 7 年前。
我需要用Sql数据库制作并包含的PHP票务系统源码用户客户端和管理员。我需要个人 CMS 的这个来源。谢谢你帮助我。 最佳答案 我在不同的情况下使用了 osticket。 这里: http://ost
我的场景:我想在日志文件中写入发生异常的部分代码(例如,发生异常的行前 5 行和行后 5 行 - 或者至少是该方法的所有代码)。 我的想法是用 C# 代码反编译 pdb 文件,并从该反编译文件中找到一
RocketMQ设定了延迟级别可以让消息延迟消费,延迟消息会使用 SCHEDULE_TOPIC_XXXX 这个主题,每个延迟等级对应一个消息队列,并且与普通消息一样,会保存每个消息队列的消费进度
先附上Hystrix源码图 在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),在Spring Cloud可以用RestTemplate+Ribbon和
此篇博客学习的api如标题,分别是: current_url 获取当前页面的url; page_source 获取当前页面的源码; title 获取当前页面的titl
? 1 2
1、前言 作为一个数据库爱好者,自己动手写过简单的sql解析器以及存储引擎,但感觉还是不够过瘾。<<事务处理-概念与技术>>诚然讲的非常透彻,但只能提纲挈领,不能让你
gory"> 目录 运行时信号量机制 semaphore 前言 作用是什么 几个主要的方法 如何实现
自己写的一个评论系统源码分享给大家,包括有表情,还有评论机制。用户名是随机的 针对某一篇文章进行评论 function subcomment() {
一、概述 StringBuilder是一个可变的字符串序列,这个类被设计去兼容StringBuffer类的API,但不保证线程安全性,是StringBuffer单线程情况下的一个替代实现。在可能的情
一、概述 System是用的非常多的一个final类。它不能被实例化。System类提供了标准的输入输出和错误输出流;访问外部定义的属性和环境变量;加载文件和库的方法;以及高效的拷贝数组中一部分元素
在JDK中,String的使用频率和被研究的程度都非常高,所以接下来我只说一些比较重要的内容。 一、String类的概述 String类的声明如下: public final class Str
一、概述 Class的实例代表着正在运行的Java应用程序的类和接口。枚举是一种类,而直接是一种接口。每一个数组也属于一个类,这个类b被反射为具有相同元素类型和维数的所有数组共享的类对象。八大基本树
一、概述 Compiler这个类被用于支持Java到本地代码编译器和相关服务。在设计上,这个类啥也不做,他充当JIT编译器实现的占位符。 放JVM虚拟机首次启动时,他确定系统属性java.comp
一、概述 StringBuffer是一个线程安全的、可变的字符序列,跟String类似,但它能被修改。StringBuffer在多线程环境下可以很安全地被使用,因为它的方法都是通过synchroni
一、概述 Enum是所有Jav中枚举类的基类。详细的介绍在Java语言规范中有说明。 值得注意的是,java.util.EnumSet和java.util.EnumMap是Enum的两个高效实现,
一、概述 此线程指的是执行程序中的线程。 Java虚拟机允许应用程序同时执行多个执行线程。 每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。 每个线程可能也可能不会被标记为守
一、抽象类Number 类继承关系 这里面的原子类、BigDecimal后面都会详细介绍。 属性和抽象方法 二、概述 所有的属性,最小-128,最大127,SIZE和BYTES代码比
我是一名优秀的程序员,十分优秀!