前言
什么是 diff?
我们写的 JSX 文件,会变成 vDom 也就是 React Element 实例,在 render 时进行调和变成 fiber,
我们知道 React 采用了双缓存,正在页面中的是 current Fiber,本次渲染的是 workInProgress Fiber,
在渲染时会用 workInProgress Fiber 直接替换 current Fiber,而 diff 做的就是比较 vDom 和 current Fiber 来生成 workInProgress Fiber,
另外在初始化的时候,不存在 current Fiber ,也就不需要进行 diff。
Fiber 的结构
先来看下 Fiber 的结构是什么样的
- return: 指向父 Fiber 节点。
- child: 指向子 Fiber 节点。
- sibling:指向兄弟 fiber 节点。
三个设计思想
- 永远只比较同层节点
- 不同节点产生不同的树,如果节点改变了,直接删除原节点,创建新节点
- 开发者通过 key 标记哪些是同个节点,在 diff 时会根据 key 进行判断(这也说明了key值必须唯一)
单节点 diff
流程
- 首先看上次更新时是否存在当前的DOM节点,如果没有证明进行了新增节点,直接创建新Fiber并返回
- 然后比较 key 值是否相等,我们可以自定义 key 值来表明哪些节点是稳定的,如果连 key 值都不相同,说明进行了删除操作,直接打上删除的 tag,创建新的 Fiber 返回
- 如果 key 相等,并且 type 也相等,证明该节点可以进行复用,直接返回即可。
- 如果 key 相等,type 不等,证明该节点发生了更新,需要将该 fiber 和他的兄弟 fiber 全部标记删除
当 key 相等,type 不等时,为什么不仅标记该 fiber 删除,还要标记兄弟 fiber 呢??
首先我们要清楚一点,这里是单节点 diff,也就是说我们当前只有一个节点,我们比较 type 前已经比较过了 key,证明了是同一个节点,如果 type 不相等的话,其他的节点根本没有比较的必要了。举个例子:
<div>
<div key="1">1</div>
<div key="2">2</div>
</div>
// 变成了
<div>
<p key="1">1</p>
</div>
在确定该节点是由 key = "1" 的 div 更新而来的,因为只有 p 一个单节点,后续也不会有节点继续和 key="1"的兄弟节点进行比较了,所以直接打上删除标签就好了。
多节点 diff
第一阶段
第一阶段注主要处理更新的节点,步骤如下
- 遍历 newChildren 和原来的 oldFiber ,首先比较 newChildren[i] 和 oldFiber,相同继续比较 newChildren[i+1] 和 oldFiber.sibling
- 如果能复用就直接复用了
- 不能复用有两种情况
- key不同:直接跳出第一阶段的遍历
- key相同,type不同,说明发生了更新,遵循第二条思想,打上删除的 tag 继续遍历
流程图
第二阶段
当经过第一阶段的遍历后可能出现以下情况
- newChildren 和 oldFiber 都遍历完了,说明不再需要第二轮的遍历了
- newChildren 没遍历完,oldFiber 遍历完,说明新增了节点,这就需要再去遍历,标记上新增tag
- newChildren遍历完,oldFiber没遍历完,说明有删除节点,所以需要遍历剩下的oldFiber,依次标记删除tag
- newChildren与oldFiber都没遍历完,这说明有节点的位置发生了变化,举个例子
oldFiber: abcd
newChildren: acbd
在第一轮遍历之后,a 可以复用,oldFiber 剩余 bcd,newChildren 剩余 cbd,此时将未处理的 oldFiber 存入以 key 为 key,oldFiber 为 value 的 Map 中,遍历 newChildren,通过newChildren[i].key就能在existingChildren 中找到 key 相同的 oldFiber,索引为 oldIndex,通过 lastPlacedIndex 来进行处理。
lastPlacedIndex 代表当前newChildren项在 oldFiber 中的索引
如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动 并将 lastPlacedIndex = oldIndex; 如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动
流程
oldFiber: abcd
newChildren: acbd
经过第一次阶段遍历
oldFiber: bcd
newChildren: cbd
查找 newChildren 的 c 在 oldFiber 中的索引为 2 ,所以 lastPlacedIndex = 2,无需换位
第一次查找后
oldFiber: bd
newChildren: bd
查找 newChildren 的 b 在 oldFiber 中的索引为 1 ,所以 lastPlacedIndex = 1,需要向右移动
第二次查找后
oldFiber: d
newChildren: d
查找 newChildren 的 b 在 oldFiber 中的索引为 3 ,所以 lastPlacedIndex = 3,无需换位
至此,Diff流程结束。