React Diff 算法
senmu2024/03/25
整体流程
全部逻辑都在 reconcileChildren(current, workInProgress, nextChildren, renderLanes)
方法中
下面对于它的入参进行解释:
current
当前节点(页面中已经展示的 DOM),初次渲染为null
workInProgress
工作节点(当前正在进行中的节点)nextChildren
子元素节点- 例如:,div 节点为工作节点时 p 节点就为他的
hello
nextChildren
- 例如:
renderLanes
优先级相关
而这个方法中又细分初次渲染和更新方法:
- 初次渲染:
mountChildFibers(workInProgress, null, nextChildren, renderLanes)
- 更新:
reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes)
- 其实都指向:
reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes)
可以看到他们的入参很相近,其实底层方法也是一样的,只不过内部会有一些不同的处理,还是老规矩,分析入参:
returnFiber
保持单链表节点的结构currentFirstChild
这里就是初次渲染和更新最大的不同,因为更新涉及到节点的复用newChild
同上lanes
优先级相关
入参分析到此为止,下面进入真正的 Diff 逻辑。
- 首先是对
Fragment
节点(<></>)进行处理,如果是该节点直接拿他的子节点 - 判断节点的类型
typof ==> 'object'
,该逻辑比较复杂,分为单节点 Diff 和多节点 Difftypeof ==> 'string'
,当作单节点处理TextNode
typeof ==> 'function'
,发出警告
- 以上类型都不是就直接标记删除
单节点 Diff
处理三种情况:
类型为
element
reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes)
- 先判断是否是初次渲染(
while(currentFirstChild !== null)
)- 初次渲染就跳过/不存在
currentFirstChild
也跳过 - 不是初次渲染就走该逻辑
- 判断
key
是否相同- 不同的话就直接删除子节点(
currentFirstChild
) - 相同的话就继续走该逻辑,判断
type
是否相同- 不同的话就连它与它的子节点直接全部删除
- 相同的话就删除该节点的兄弟节点,复用该节点,返回该节点,退出逻辑 🔚
- 不同的话就直接删除子节点(
- 赋值子节点为它的兄弟节点(
currentFirstChild = currentFirstChild.sibling
),继续上面的逻辑👆(注意这里的处理与key
相同但type
不同的处理逻辑不同)
- 初次渲染就跳过/不存在
- 根据
newChild
的类型,创建对应节点,返回该节点,退出逻辑 🔚
类型为
portal
- 与上面的逻辑类似
类型为
lazy
- 执行下面逻辑
var payload = newChild._payload; var init = newChild._init; // TODO: This function is supposed to be non-recursive. return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes);
不是这三种类型的话就接着继续判断是否多节点
流程图如下:
多节点 Diff
源码中的方法为:reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes)
- 先校验
key
- 然后处理更新的情况
key
不同,直接跳过本次循环key
相同type
不同,表示无法复用,需要创建新节点type
相同,表示可以复用,直接复用
- 再处理新增(老节点无法复用)的情况
- 如果没有老节点的话就直接创建新节点
- 最后如果新节点还未处理完的话再将老节点未被删除(标记为删除)的节点集合为 Map 数据,从中找到可以复用的节点
- 因为
key
不同跳出第一次循环会导致新节点没被处理完,那就需要考虑移动复用节点的情况了 - React 的处理是将所有的旧节点重新 push,举个例子:
- 假如列表为 1->2->3,我们更新后变为 3->1->2;
- 看起来是将 3 移动到最前面,实际上是 3 不动,依次向后面添加 1 与 2
- 同理如果列表从 1->2->3 变为 1->3->2
- 那么就是 1 与 3 不动,只将 2 添加到 3 后面就好了
- 假如列表为 1->2->3,我们更新后变为 3->1->2;
- 因为
流程图如下:
疑问🤔
lazy
的组件_palyload
和_init
是什么?