1 react降低diff复杂度所做假设
- 只对同级元素进行diff
- 两个不同类型的元素会产生不同的树
- 开发者可以通过key来暗示哪些子元素在不同渲染下保持稳定
2 是单节点还是多节点diff?
通过reconcileChildFibers函数的newchild参数将diff分为两类:
- 当newchild为object、number或者string时,为单节点
- 当newchild为array时,为多节点
3 单节点diff(reconcileSingleElement)
只有当key和type都相同才可以被复用,如果第一次有三个元素,更新后仅有一个元素
- 当key相同type不同时,使用deleteRemainingChildren将chlid及起兄弟元素全部标记为删除
- 当key不同时type相同,使用deleteChild仅将child标记删除
4 多节点diff
diff分为新增,删除和更新,在日常开发中,更新出现的频率最多,所以diff会优先判断当前节点是否属于更新,所以react diff算法的整体逻辑,会被分为两次遍历:
- 处理更新的节点
- 处理剩下不属于更新的节点
4.1 第一轮遍历
- let i = 0, 对比newchlidren[i]和oldFiber,判断是否可复用
- 如果可复用,i++,对比newchildren[i]和oldFiber.sibling,如果可以复用则继续遍历
- 如果不可复用,跳出整个遍历,结束第一轮循环
- 如果newChildren或者oldFiber遍历完,结束第一轮循环
4.2 第二轮遍历
第一轮遍历后,总共会有如下几种情况
- newChildren和oldFiber全部遍历完,说明只有组件更新
- newChildren没有遍历完,oldFiber遍历完了,说明有新节点插入。只需要遍历剩下的newChildren为生成的workInprogress Fiber依次标记placement
- newChildren遍历完,oldFiber没有遍历完,说明有节点被删除,遍历剩余oldFiber,依次标记Deletion
- newChildren和oldFiber都没有遍历完,这说明有节点在本次更新中改变了位置.由于位置发生了改变,所以不能使用位置作为索引。为了快速找到key对应的oldFiber,我们把还未处理的oldFiber存入以key为key,oldFiber为value的map里,接下来遍历剩余newChildren就可以快速处理节点移动的情况了
4.3.1 标记节点是否移动
既然要对比节点是否被移动,那就要有参照物,我们的参照物就是最后一个可复用节点在oldFiber中的位置(用lastPlacedIndex表示),如果oldIndex < lastPlacedIndex,则说明元素需要向右移动,如果oldIndex >= lastPlacedIndex ,则lastPlacedIndex = oldIndex。
5.1 示例1
// 更新前
abcd
// 更新后
acdb
===============================第一轮循环==================================
a(更新后) vs a(更新前) key相同,可以复用 oldIndex = 0, lastplacedIndex = 0
继续第一轮遍历
b(更新后) vs c(更新前) key不同,不可复用 跳出第一轮循环
===============================第一轮循环==================================
===============================第二轮循环==================================
newChildren ===> cdb oldFiber ===> bcd 都有剩余,不需要执行新增或者删除节点操作
将剩余oldFiber保存为map
// 移动规则
// oldIndex >= lastPlacedIndex, 不需要移动, 并将lastPlacedIndex = oldIndex
// oldIndex < lastPlacedIndex, 可复用节点之前位置小于当前更新后需要插入的位置,需要右移
// 当前oldFiber bcd
// 当前newChildren cdb
继续遍历newChildren
key === c 在oldFiber中存在,oldIndex = 2, oldIndex > lastPlacedIndex c节点位置不变
lastPlacedIndex = oldIndex = 2
继续遍历newChildren
key === d 在oldFiber中存在,oldIndex = 3, oldIndex > lastPlacedIndex d节点位置不变
lastPlacedIndex = oldIndex = 3
继续遍历newChildren
key === b 在oldFIber中存在, oldIndex = 1, oldIndex < lastPlacedIndex b节点右移
===============================第二轮循环====================================
最后acd不变,b被标记右移
5.2 示例2
// 更新前
abcd
// 更新后
dabc
===============================第一轮循环==================================
d(更新后) vs a(更新前) key不同,不可复用 跳出第一轮循环
===============================第一轮循环==================================
===============================第二轮循环==================================
newChildren ===> dabc oldFiber ===> abcd 都有剩余,不需要执行新增或者删除节点操作
将剩余oldFiber保存为map
// 移动规则
// oldIndex >= lastPlacedIndex, 不需要移动, 并将lastPlacedIndex = oldIndex
// oldIndex < lastPlacedIndex, 可复用节点之前位置小于当前更新后需要插入的位置,需要右移
// 当前oldFiber abcd
// 当前newChildren dabc
继续遍历newChildren
key === d 在oldFiber中存在,oldIndex = 3, oldIndex > lastPlacedIndex d节点位置不变
lastPlacedIndex = oldIndex = 3
继续遍历newChildren
key === a 在oldFiber中存在,oldIndex = 0, oldIndex < lastPlacedIndex a节点右移
lastPlacedIndex = 3
继续遍历newChildren
key === b 在oldFIber中存在, oldIndex = 1, oldIndex < lastPlacedIndex b节点右移
lastPlacedIndex = 3
继续遍历newChildren
key === c 在oldFIber中存在, oldIndex = 2, oldIndex < lastPlacedIndex c节点右移
===============================第二轮循环====================================
最后d不变,abc被标记右移
参考:React 技术揭秘