React Diff揭秘

1 react降低diff复杂度所做假设

  • 只对同级元素进行diff
  • 两个不同类型的元素会产生不同的树
  • 开发者可以通过key来暗示哪些子元素在不同渲染下保持稳定

2 是单节点还是多节点diff?

通过reconcileChildFibers函数的newchild参数将diff分为两类:

  1. 当newchild为object、number或者string时,为单节点
  2. 当newchild为array时,为多节点

3 单节点diff(reconcileSingleElement)

只有当key和type都相同才可以被复用,如果第一次有三个元素,更新后仅有一个元素

  • 当key相同type不同时,使用deleteRemainingChildren将chlid及起兄弟元素全部标记为删除
  • 当key不同时type相同,使用deleteChild仅将child标记删除

4 多节点diff

diff分为新增,删除和更新,在日常开发中,更新出现的频率最多,所以diff会优先判断当前节点是否属于更新,所以react diff算法的整体逻辑,会被分为两次遍历:

  1. 处理更新的节点
  2. 处理剩下不属于更新的节点

4.1 第一轮遍历

  1. let i = 0, 对比newchlidren[i]和oldFiber,判断是否可复用
  2. 如果可复用,i++,对比newchildren[i]和oldFiber.sibling,如果可以复用则继续遍历
  3. 如果不可复用,跳出整个遍历,结束第一轮循环
  4. 如果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 标记节点是否移动

既然要对比节点是否被移动,那就要有参照物,我们的参照物就是最后一个可复用节点在oldFiber中的位置(用lastPlacedIndex表示),如果oldIndex < lastPlacedIndex,则说明元素需要向右移动,如果oldIndex >= lastPlacedIndex ,则lastPlacedIndex = oldIndex。

5 示例

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 技术揭秘

猜你喜欢

转载自blog.csdn.net/qq_39544148/article/details/128616367