我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
React的diff算法,根据节点数量可以简单明了的分为两种:
单节点diff算法
多节点diff算法
1. 单节点diff算法
单点diff算法,场景比较单一、简单 先根据key(jsx的key属性)是否相同,如果相同再进行type是否也相同,如果两者都相同,该dom节点才可以复用
2. 多点diff算法
多节点diff算法,顾名思义,多个dom节点进行比较;总得来说,一共分为三个场景
1.节点更新
1.1节点属性变化
1.2 节点类型变化
2.节点新增或减少
2.1 新增节点
2.2 删除节点
3.节点位置变化
节点的更新,我们都知道一共是三种:
- 新增
- 删除
- 更新
React团队在日常开发中发现,更新在日常开发中发生的频率要比其他两种的频率更高,所以diff在处理节点时会优先判断该节点的状态是否是更新状态。
又因为React特殊的单链表结构,无法进行双指针优化,所有diff算法根据更新进行两轮遍历。
第一轮:处理更新的节点
第二轮:处理剩下的节点
具体实现
第一轮遍历
在第一轮遍历中,
会将新的节点数组,newChildren
,将其每一个节点按照从左到右的顺序,依次与oldFiber进行比较,判断dom节点是否可以复用(复用规则参考单节点,key和type是否都一样);
如果可以复用,继续将剩余的newChildren
中的节点们和oldFiber.sibling
(兄弟节点进行比较),如果可以复用,就一直遍历下去;
如果不可以复用,这里,不可复用将会出现两种情况:
key
不同导致不可以复用,将会立刻跳出整个遍历,第一轮遍历马上结束(key不同,老厉害了);key
相同但是type不同(key一样,但是type从div变成了span),会将oldFiber标记为DELETION(删除),然后继续进行遍历;
如果newChildren
遍历完或者oldFiber遍历完,将会跳出第一轮遍历
看完了刚刚的描述,我们知道了在多节点diff算法中,第一轮遍历,也就是处理更新的节点这一轮遍历中,具体如何进行复用dom节点,但是我们也有几种特殊的情况,是第一轮无法处理的,
如我们上面说的key
节点不同(因为跳出了第一轮遍历),newChildren
或者oldFiber
两者没有遍历完(这里又可以分为三种情况:1.前者遍历完;2.后者遍历完;3.两者同时遍历完)
带着这些疑问,我们进行第二轮的遍历
第二轮遍历
这里我们,根据第一轮遍历的结果进行处理:
-
如果是
newChildren
和oldFiber
同时都遍历完了,皆大欢喜,在第一轮diff就会结束; -
newChildren
没遍历完,但是oldFiber
遍历完了 这说明已经有的dom都已经复用完了,剩下的newChildren
中的节点,都是新增的节点,我们只需要遍历剩下的节点,然后为它们打上Placement
的标记就好了 -
newCHildren
遍历完了,oldFiber没有遍历完 这说明更新后,新的比老的少了,有节点被删除了。也就是没有遍历到的oldFiber节点,我们依次给它们打上Deletion
就可以了 -
newChildren与oldFiber都没有遍历完
这个场景是diff算法中,最最复杂的场景,因为这种场景意味着出现了移动的节点。
因为有节点变更了位置,所以我们前面用的下标,就变得不可靠了!
那么我们还能用什么东东呢?
key
,我们要使用key,为了快速找到key对应的oldFiber,我们将所有还没有处理的oldFiber存入以该节点的key为key,oldFiber为value的Map中
// React源码
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
然后我们遍历我们刚刚还没有用完的newChildren
,通过该数组中每个节点的key去上述map
中找到对应key的oldFiber
。
在这里,我们首先要知道:节点是否移动是通过啥判断的,也就是说节点移动的参照是什么??
通过学习,得知,参照物是:最后一个可复用的节点在oldFiber中的位置索引
react中,用lastPlacedIndex
表示!!!
这开始之前,我们需要明确,可复用节点和lastPlacedIndex
的关系,以及oldIndex
我们知道,在前面的遍历过程中,我们根据newChildren
的顺序进行遍历,那么lastPlacedIndex
一定是最后一个可复用的节点,也就是newChildren
数组中,所能复用的最后右边的节点的下标;
变量oldIndex
表示遍历到可复用节点在oldFiber
中的索引位置;
所以我们根据比较两个下标,来进行判断节点如何进行移动:
如果oldFiber
的值 <
lastPlacedIndex
,则代表,这个节点,需要向右边移动
刚开始,lastPlacedIndex
默认值为0
,每有一个可以复用的节点,并且oldIndex
>=
lastPlacedIndex
,会将前者的值赋值给后者。
一个小demo🌰:
//前
//oldFiber
<div key='1'>前端江鸟1</> //1
<div key='2'>前端江鸟2</> //2
<div key='3'>前端江鸟3</> //3
<div key='4'>前端江鸟4</> //4
//后
//newChildren
<div key='1'>前端江鸟1</> //1
<div key='3'>前端江鸟3</> //3
<div key='4'>前端江鸟4</> //4
<div key='2'>前端江鸟2</> //2
第一轮遍历:
为了方便演示,我们用注释代表节点
第一轮:
前后对比,1的key
和type
一样,可以复用,此时oldIndex
= 0;lastPlaceIndex
= 0;
继续: 发现2不等于3,key不相同,无法复用,直接跳出第一轮遍历;
第二轮遍历:
开始前,我们知道当前的lastPlaceIndex = 0!!!
根据前文所说,newChildren
和oldFiber
都没有遍历完,触发率最复杂的第四种场景!
😄😄😄
开始:
//前
//oldFiber
<div key='2'>前端江鸟2</> //2
<div key='3'>前端江鸟3</> //3
<div key='4'>前端江鸟4</> //4
//后
//newChildren
<div key='3'>前端江鸟3</> //3
<div key='4'>前端江鸟4</> //4
<div key='2'>前端江鸟2</> //2
将剩余oldFiber保存为map(key是节点的key,value是节点)
newChildren
根据下标依次遍历,发现key = 3
在oldFiber
中存在,这时,oldIndex = 2
;此时lastPlacedIndex = 0
;发现oldIndex > lastPlacedIndex
,按照前文所说,该节点位置不变,这时,lastPlaceIndex = oldIndex = 2
;
继续遍历:
key = 4
在oldFiber
中存在,这时oldIndex = 3
,此时lastPlacedIndex = 2
,发现oldIndex > lastPlacedIndex
,按照前文所说,该节点位置不变,这时,lastPlaceIndex = oldIndex = 3
;
继续遍历:
key = 2
在oldFiber
中存在,oldIndex = 1
,此时lastPlaceIndex = 3
,发现oldIndex < lastPlaceIndex
,按照前文所说,该节点位置变更了,该节点向右移动了!!
oldFiber和newChildren都遍历完了,皆大欢喜!!!
最后,根据结果,发现,134都没有位移,2发生了向右位移!!
总结
React在diff算法中,如何复用,一共就这些情况,这里尽量不涉及源码,只说思想,争取明白diff的思想,明白diff如何复用dom节点!!! 最后,如果有不对的地方,欢迎指正,共同进步!!