You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
如果 key 或者 type 不同,则给旧的 fiber 添加删除标记,并生成新的 fiber 节点
第一种场景 type 不同
// 旧的 fiber 树<div><h1key="null">h1</h1></div>// 更新后的 element tree<div><h2key="null">h2</h2></div>
第二种场景 多节点变为单节点
// 旧的 fiber 树<div><h1key="h1-key">h1</h1><h2key="h2-key">h2</h2></div>// 更新后的 element tree<div><h2key="h2-key">h2</h2></div>
首先比较 key
h2-key 和 h1-key 不同,则 h1-key 标记为删除
继续遍历旧的 fiber 树,h2-key 相同,同时 type 相同,则可以复用
如果旧的 fiber 树中,h2 后面还有 h3,h4等,依然只是保留 h2,将其他的删除
<div><h1key="h1-key">h1</h1><h2key="h2-key">h2</h2><h3key="h3-key">h3</h3><h4key="h4-key">h4</h4></div>// 更新后的 element tree<div><h2key="h2-key">h2</h2></div>
<div><h1key="h1-key">h1</h1><pkey="h2-key">h2</p><h3key="h3-key">h3</h3><h4key="h4-key">h4</h4></div>// 更新后的 element tree<div><h2key="h2-key">h2</h2></div>
比较 h1-key,发现 key 不同,则 h1 标记为删除,继续遍历
比较 h2-key 发现 key 相同,比较 type 发现不同,则标记 p 删除,由于已经找到相同的 key,根据 react 的假设,已经没有必要再继续遍历下去了,因此 h3 和 h4 标记为删除。重新创建一个 h2 节点
React17 中 DOM DIFF 算法动机
在 React17+中,DOM DIFF 就是根据当前显示的页面对应的 Fiber 树和
render
函数生成的最新的element tree
对比生成新的 Fiber 树的过程。然而将一棵树转换成另一棵树的最小操作次数,即使使用最优的算法,该算法的复杂程度仍为 O(n^3 ),其中 n 是树中元素的数量如果在
React
中使用该算法,那么展示 1000 个元素则需要 10 亿次比较。这个开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n)的启发式算法:Diffing 算法
在整个 diffing 过程中,同时使用 type 和 key 判断是否复用,首先判断 key,其次判断 type
为什么需要 key
在子元素列表末尾新增元素时,更新开销比较小,比如:
React 会先匹配两个
<li>first</li>
对应的树,然后匹配第二个元素<li>second</li>
对应的树,最后插入第三个元素的<li>third</li>
树。如果只是简单的将新增元素插入到表头,那么更新开销会比较大,比如:
React 并不会意识到应该保留
<li>Duke</li>
和<li>Villanova</li>
,而是会重建每一个子元素。这种情况会带来性能问题可以得知:
插入表尾比表头性能要好很多
因此我们可以使用
key
告诉react
复用元素单节点
记住,这里是指
render
函数重新生成的element tree
的子节点只有一个的情况单节点,只有
type
和key
都相同才可以复用,否则重新创建一个新的节点如果新的
element tree
子节点只有一个元素:第一种场景 type 不同
第二种场景 多节点变为单节点
首先比较
key
h2-key
和h1-key
不同,则h1-key
标记为删除h2-key
相同,同时 type 相同,则可以复用如果旧的 fiber 树中,
h2
后面还有h3
,h4
等,依然只是保留h2
,将其他的删除当遍历到
h2-key
时,发现key
和type
都相同,因此h2
可以复用,此时已经没有必要再继续比较接下来的节点,因此h3
和h4
都标记为删除如果
key
相同,type
不同:h1-key
,发现key
不同,则h1
标记为删除,继续遍历h2-key
发现key
相同,比较type
发现不同,则标记p
删除,由于已经找到相同的key
,根据 react 的假设,已经没有必要再继续遍历下去了,因此h3
和h4
标记为删除。重新创建一个h2
节点多节点
注意,这里是指
render
函数重新生成的element tree
的子节点有多个的情况同理,多节点的情况,也是只有
type
和key
都相同,才能复用,否则重新创建节点第一种情况:更新
全部子节点都可以复用,只需要更新即可,这种只需要一轮循环
第二种情况:key 相同,type 不同
在这种情况中,由于
key
相同而type
不同导致不可复用,则将 旧的 fiber 节点标记为删除,并继续遍历,此时不会跳出第一轮循环首先判断
key
A-key
相同,但是type
不同,因此将<li key="A-key">A</li>
标记为删除,重新创建<div key="A-key">A-new</div>
节点key
和type
都相同,因此都可以复用第三种情况:key 不同,退出第一轮循环
如果第一轮遍历的时候,发现 key 不一样,则立刻跳出第一轮循环。key 不一样,说明可能有位置变化
第一轮循环:
A-key
相同并且type
相同,能复用,更新 A 就可以B-key
和C-key
不同,立即退出第一轮循环,初始化lastPlacedIndex
为旧的 fiberA-key
的索引。lastPlacedIndex
表示最近的一个可复用的节点在 旧 fiber 节点中的位置索引B-key
节点开始遍历旧的 fiber 节点,并构造一个 map:fiberMap
的键值是元素的key
,值对应元素的fiber
节点C-key
在fiberMap
中存在,并且旧的C-key
节点的oldIndex = 2 大于 lastPlacedIndex = 0
,因此C-key
不需要移动,标记更新即可。将lastPlacedIndex
设置为C-key
的oldIndex
,此时lastPlacedIndex = 2
。同时将C-key
从fiberMap
中删除,最终得到E-key
在fiberMap
中存在,同时E-key
节点的oldIndex
也大于lastPlacedIndex
,同C-key
一样不需要移动,标记为更新即可,最终得到:B-key
在fiberMap
中存在,需要注意,由于B-key
的oldIndex
小于lastPlacedIndex
,因此这个节点需要标记为移动并且更新,最终得到:G-key
在fiberMap
中不存在,因此这是一个新增的节点到此,新的节点已经遍历完成,将
fiberMap
中剩余的旧节点都标记为删除最后,在 commit 阶段,先删除 D 和 F,再更新 A、C 和 E,然后移动并更新 B,最后插入 G
第四种情况:极端场景,前面的节点都需要移动
这种场景将后面的节点移动到了前面,性能不好,因此应该避免这种写法
第一轮遍历:
D-key
和A-key
不同,key
改变了,不能复用,跳出第一轮循环第二轮循环
初始化
lastPlacedIndex = 0
,并创建一个旧的 fiber 节点的映射D-key
在fiberMap
中存在,并且oldIndex > lastPlacedIndex
,因此可以复用并且标记为更新,不需要移动lastPlacedIndex
都大于这些节点的oldIndex
,因此这些节点都需要标记为移动并且更新从中可以看出,这种更新,react 并不会仅仅将 D-key 节点移动到前面,而是将 A-key,B-key,C-key 都移动到 D-key 后面。
因此我们需要避免将节点从后面移动到前面的操作
The text was updated successfully, but these errors were encountered: