Open Twlig opened 2 years ago
为了防止概念混淆,这里再强调下
一个DOM节点在某一时刻最多会有4个节点和他相关。 current Fiber。如果该DOM节点已在页面中,current Fiber代表该DOM节点对应的Fiber节点。 workInProgress Fiber。如果该DOM节点将在本次更新中渲染到页面中,workInProgress Fiber代表该DOM节点对应的Fiber节点。 DOM节点本身。 JSX对象。即ClassComponent的render方法的返回结果,或FunctionComponent的调用结果。JSX对象中包含描述DOM节点的信息。 Diff算法的本质是对比1和4,生成2。
一个DOM节点在某一时刻最多会有4个节点和他相关。
DOM节点
current Fiber
Fiber节点
workInProgress Fiber
JSX对象
ClassComponent
render
FunctionComponent
Diff算法的本质是对比1和4,生成2。
Diff算法
由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量。
Diff
n
如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。
React
为了降低算法复杂度,React的diff会预设三个限制:
diff
div
p
key prop
// 更新前 <div> <p key="ka">ka</p> <h3 key="song">song</h3> </div> // 更新后 <div> <h3 key="song">song</h3> <p key="ka">ka</p> </div>
如果没有key,React会认为div的第一个子节点由p变为h3,第二个子节点由h3变为p。这符合限制2的设定,会销毁并新建。
key
h3
但是当我们用key指明了节点前后对应关系后,React知道key === "ka"的p在更新后还存在,所以DOM节点可以复用,只是需要交换下顺序。
key === "ka"
我们从Diff的入口函数reconcileChildFibers出发,该函数会根据newChild(即JSX对象)类型调用不同的处理函数。
reconcileChildFibers
newChild
我们可以从同级的节点数量将Diff分为两类:
object
number
string
Array
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber { const key = element.key; let child = currentFirstChild; // 首先判断是否存在对应DOM节点 while (child !== null) { // 上一次更新存在DOM节点,接下来判断是否可复用 // 首先比较key是否相同 if (child.key === key) { // key相同,接下来比较type是否相同 switch (child.tag) { // ...省略case default: { if (child.elementType === element.type) { // type相同则表示可以复用 // 返回复用的fiber return existing; } // type不同则跳出switch break; } } // 代码执行到这里代表:key相同但是type不同 // 将该fiber及其兄弟fiber标记为删除 deleteRemainingChildren(returnFiber, child); break; } else { // key不同,将该fiber标记为删除 deleteChild(returnFiber, child); } child = child.sibling; } // 创建新Fiber,并返回 ...省略 }
可能会奇怪为啥,key相同,type不同需要全部删除。这是因为,咱们此次更新的是单节点。当key相同(唯一性),就说明对上了上一次的节点,后续的兄弟节点不可能对上,但是由于type不同,这个节点不能复用,就需要删除。当key不同,只是当前节点没对上,后续的兄弟节点还有可能对上。
让我们来做几道习题巩固下吧:
请判断如下JSX对象对应的DOM元素是否可以复用:
DOM
// 习题1 更新前 <div>ka song</div> // 更新后 <p>ka song</p> // 习题2 更新前 <div key="xxx">ka song</div> // 更新后 <div key="ooo">ka song</div> // 习题3 更新前 <div key="xxx">ka song</div> // 更新后 <p key="ooo">ka song</p> // 习题4 更新前 <div key="xxx">ka song</div> // 更新后 <div key="xxx">xiao bei</div>
。
公布答案:
习题1: 未设置key prop默认 key = null;,所以更新前后key相同,都为null,但是更新前type为div,更新后为p,type改变则不能复用。
key = null;
null
type
习题2: 更新前后key改变,不需要再判断type,不能复用。
习题3: 更新前后key改变,不需要再判断type,不能复用。
习题4: 更新前后key与type都未改变,可以复用。children变化,DOM的子元素需要更新。
children
多节点Diff分为:第一轮遍历,第二轮遍历两种。第一遍是找出待更新的DOM,第二遍是找出待新增,删除,移动的DOM。(更新的场景更多,所以react官方先判断更新)
第一轮遍历:处理更新的节点。
更新
第二轮遍历:处理剩下的不属于更新的节点。
第一轮遍历步骤如下:
let i = 0
newChildren
newChildren[i]
oldFiber
i++
oldFiber.sibling
DELETION
i === newChildren.length - 1
oldFiber.sibling === null
对于第一轮遍历的结果,我们分别讨论:
那就是最理想的情况:只需在第一轮遍历进行组件更新 。此时Diff结束。
已有的DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。
workInProgress fiber
Placement
意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的oldFiber,依次标记Deletion。
Deletion
这意味着有节点在这次更新中改变了位置。需要移动位置。这是Diff算法最精髓也是最难懂的部分。我们接下来会重点讲解。
由于有节点改变了位置,所以不能再用位置索引i对比前后的节点,那么如何才能将同一个节点在两次更新中对应上呢?
i
我们需要使用key。
为了快速的找到key对应的oldFiber,我们将所有还未处理的oldFiber存入以key为key,oldFiber为value的Map中。
Map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
在Demo中我们简化下书写,每个字母代表一个节点,字母的值代表节点的key
// 之前 abcd // 之后 acdb ===第一轮遍历开始=== a(之后)vs a(之前) key不变,可复用 此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0 所以 lastPlacedIndex = 0; 继续第一轮遍历... c(之后)vs b(之前) key改变,不能复用,跳出第一轮遍历 此时 lastPlacedIndex === 0; ===第一轮遍历结束=== ===第二轮遍历开始=== newChildren === cdb,没用完,不需要执行删除旧节点 oldFiber === bcd,没用完,不需要执行插入新节点 将剩余oldFiber(bcd)保存为map // 当前oldFiber:bcd // 当前newChildren:cdb 继续遍历剩余newChildren key === c 在 oldFiber中存在 const oldIndex = c(之前).index; 此时 oldIndex === 2; // 之前节点为 abcd,所以c.index === 2 比较 oldIndex 与 lastPlacedIndex; 如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动 并将 lastPlacedIndex = oldIndex; 如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动 在例子中,oldIndex 2 > lastPlacedIndex 0, 则 lastPlacedIndex = 2; c节点位置不变 继续遍历剩余newChildren // 当前oldFiber:bd // 当前newChildren:db key === d 在 oldFiber中存在 const oldIndex = d(之前).index; oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3 则 lastPlacedIndex = 3; d节点位置不变 继续遍历剩余newChildren // 当前oldFiber:b // 当前newChildren:b key === b 在 oldFiber中存在 const oldIndex = b(之前).index; oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1 则 b节点需要向右移动 ===第二轮遍历结束=== 最终acd 3个节点都没有移动,b节点被标记为移动
原文链接:https://react.iamkasong.com/
Diff算法
为了防止概念混淆,这里再强调下
Diff的瓶颈以及React如何应对
由于
Diff
操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n
是树中元素的数量。如果在
React
中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。为了降低算法复杂度,
React
的diff
会预设三个限制:Diff
。如果一个DOM节点
在前后两次更新中跨越了层级,那么React
不会尝试复用他。div
变为p
,React会销毁div
及其子孙节点,并新建p
及其子孙节点。key prop
来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:如果没有
key
,React
会认为div
的第一个子节点由p
变为h3
,第二个子节点由h3
变为p
。这符合限制2的设定,会销毁并新建。但是当我们用
key
指明了节点前后对应关系后,React
知道key === "ka"
的p
在更新后还存在,所以DOM节点
可以复用,只是需要交换下顺序。Diff是如何实现的
我们从
Diff
的入口函数reconcileChildFibers
出发,该函数会根据newChild
(即JSX对象
)类型调用不同的处理函数。我们可以从同级的节点数量将Diff分为两类:
newChild
类型为object
、number
、string
,代表同级只有一个节点newChild
类型为Array
,同级有多个节点单节点Diff
可能会奇怪为啥,key相同,type不同需要全部删除。这是因为,咱们此次更新的是单节点。当key相同(唯一性),就说明对上了上一次的节点,后续的兄弟节点不可能对上,但是由于type不同,这个节点不能复用,就需要删除。当key不同,只是当前节点没对上,后续的兄弟节点还有可能对上。
练习题
让我们来做几道习题巩固下吧:
请判断如下
JSX对象
对应的DOM
元素是否可以复用:。
。
。
。
公布答案:
习题1: 未设置
key prop
默认key = null;
,所以更新前后key相同,都为null
,但是更新前type
为div
,更新后为p
,type
改变则不能复用。习题2: 更新前后
key
改变,不需要再判断type
,不能复用。习题3: 更新前后
key
改变,不需要再判断type
,不能复用。习题4: 更新前后
key
与type
都未改变,可以复用。children
变化,DOM
的子元素需要更新。多节点Diff
多节点Diff分为:第一轮遍历,第二轮遍历两种。第一遍是找出待更新的DOM,第二遍是找出待新增,删除,移动的DOM。(更新的场景更多,所以react官方先判断更新)
第一轮遍历:处理
更新
的节点。第二轮遍历:处理剩下的不属于
更新
的节点。第一轮遍历
第一轮遍历步骤如下:
let i = 0
,遍历newChildren
,将newChildren[i]
与oldFiber
比较,判断DOM节点
是否可复用。i++
,继续比较newChildren[i]
与oldFiber.sibling
,可以复用则继续遍历。key
不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。key
相同type
不同导致不可复用,会将oldFiber
标记为DELETION
,并继续遍历newChildren
遍历完(即i === newChildren.length - 1
)或者oldFiber
遍历完(即oldFiber.sibling === null
),跳出遍历,第一轮遍历结束。第二轮遍历
对于第一轮遍历的结果,我们分别讨论:
1 .
newChildren
与oldFiber
同时遍历完那就是最理想的情况:只需在第一轮遍历进行组件
更新
。此时Diff
结束。2.
newChildren
没遍历完,oldFiber
遍历完已有的
DOM节点
都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren
为生成的workInProgress fiber
依次标记Placement
。3.
newChildren
遍历完,oldFiber
没遍历完意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的
oldFiber
,依次标记Deletion
。4.
newChildren
与oldFiber
都没遍历完这意味着有节点在这次更新中改变了位置。需要移动位置。这是
Diff算法
最精髓也是最难懂的部分。我们接下来会重点讲解。处理移动的节点
由于有节点改变了位置,所以不能再用位置索引
i
对比前后的节点,那么如何才能将同一个节点在两次更新中对应上呢?我们需要使用
key
。为了快速的找到
key
对应的oldFiber
,我们将所有还未处理的oldFiber
存入以key
为key,oldFiber
为value的Map
中。Demo1
在Demo中我们简化下书写,每个字母代表一个节点,字母的值代表节点的
key
原文链接:https://react.iamkasong.com/