[React Origin Code] 2022年来聊聊React Diff -- reconcileChildFibers
React Diff -- Reconcile
希望可以帮助到,与我同样纠结在此的前端程序员们。
大家都知道,React
真正的Diff
逻辑是在reconcileChildFibers
,这没有任何问题,那么图片当中的两行代码是什么?与Diff
有什么关系,事实上 reconcileChildFibers
被当作闭包函数返回了出来,被React用于两个方面,第一个方面是初次挂载渲染,第二个方面是更新Diff渲染。那我们接下来就从Reconcile阶段的初次渲染
和更新Diff
两个方面来说React Diff
。
初次渲染
我们先对初次渲染做总结,之后看代码会很有感觉:React的初次渲染在reconcile阶段,主要的任务是从根节点FiberRoot
开始深度优先遍历多叉树结构的vdom
,映射构建单层fiber链表结构和整个fiber多叉树结构(及构建fiber对象的属性,包括但不限于index, key, type,internate, flags……),并且构建fiber
结构的child
(第一个孩子), return
(父亲), sibling
(兄弟)关系。
下面是reconcile构建的过程源码
这里我们只是简单聊聊reconcile阶段的初次渲染,暂时不涉及到React的scheduler
和commitWork
阶段。有人可能会问,文章到了这里没出现Diff的过程,那么初次渲染和Diff有什么关系呢?其实答案很简单,大家也都知道,有了初次的渲染,我们下次更新才有Diff的对象呀! 接下来我们就来和大家聊聊更新阶段的Diff
。
更新Diff渲染
我们先对更新Diff渲染
做总结,之后看代码会很有感觉:React的更新Diff渲染在reconcile阶段,主要的任务是将不同类别(type)旧的fiber
节点和新的vdom
结构进行Diff
,可以分为:对普通文本,数字节点进行reconcile
,对单节点进行reconcile
,对多节点进行reconcile
普通string, number节点的 reconcile
-
进入这个函数,已经确定
newChildren
是文本节点了。-
这里判断
oldFiber
是不是文本节点。 -
如果是文本节点,可以复用节点,就使用
useFiber
来复用创建新的文本fiber
节点。 -
如果不是文本节点,不可以复用节点,删除当前层级的
oldFiber
链表,只创建一个新的文本fiber
节点。
-
单节点的 reconcile
- 进入这个函数,已经确定
newChildren
是单节点了。-
这个时候判断
oldFiber
的key
和type
,看看可不可以复用。 -
可以复用,删除
oldFiber
链表剩余的链表节点。 -
不可以复用,删除
oldFiber
链表的全部节点,创建新的fiber
节点。
-
多节点的 reconcile
多节点是Diff
的精髓,我们还是先总结,等一下看代码会非常有感觉,多节点的diff的主要流程是:三次循环遍历,一次判断,完成oldFiber链表和新的vdom结构的diff,从而完成oldfiber链表节点的删除和移动和newfiber节点的增加,以构造新的fiber结构。
第一次循环遍历
-
如果第一次循环遍历的顺利的话,大家看for循环的退出条件,也就是
newChldren
都可以复用oldFIber
链表节点或者是oldFiber
有的链表节点,newChldren
都可以复用。那么newChldren
就可以顺利建立起child, sibling的联系,所以在顺利的情况下,第一次循环退出的条件要不就是newChldren
的长度太长了,要不就是oldFiber
的链表长度太长了。- 如果是
oldFiber
的链表长度太长了,经过第一次判断,会delete
掉多余的oldFiber
链表节点。 - 如果是
newChldren
的长度太长了,在经过第二次循环遍历会构建没有遍历到的newChldren
链表节点。 - 自此结束,如果顺利的话不会进入第三次循环。
- 如果是
-
如果第一次遍历循环不顺利的话,即只要发现一个不能复用的节点,就立马退出第一次循环。从不能复用的节点开始,将所有的
oldfiber
的链表节点,依次加入到map
结构当中。-
然后进入第三次循环,继续遍历
newChldren
链表,遍历到一个newChldren, jsx
节点,就拿着key
去由oldFiber
节点构成的map
当中去找 可以复用的key
。- 没有找到,构建新的
fiber
节点,flag
为PLACEMENT
。 - 如果
newChldren
在map
当中找到对应key
的fiber
了,说明可以复用oldfiber
节点。 - 复用分为两种:一种是移动复用,一种是更新复用。
- 如果是更新复用:那么就
updateElement(oldChildrenVdom, newChildrenVdom)
复用构建fiber节点,然后在map
当中删除复用的oldFiber
。 - 如果是移动复用:那么就利用
placedIndex
索引来移动节点,flag
isMOVE
。这里我当时最大的疑问就是怎么样通过placedIndex
来移动的。源码当中的index < placedIndex
为什么会被标注为移动? 原因很简单就是两个节点的相对位置发送了变化,比如原来是a -> d
现在在是d -> a
, 我们从现在的d -> a
开始出发遍历,遍历到d
,发现在原来的1
位置,此时placeIndex
更新为1
,当遍历到a
的时候,我们发现a
在原来的0
位置,如果相对位置没有发送变化,这里a
的位置应该在b
的后面。大于1 (placeIdex)
,但是现在是< 1
意味着a
到了b
前面,所以相对位置发送了变化,标记为移动。然后在map
当中删除复用的oldFiber
。之后删除map
当中剩余的fiber
节点。 - 并循环移动和更新来构建出这一层新的
fiber
结构。
- 没有找到,构建新的
-
一次判断
- 和上文第一次判断那里说的一样,当
newFiber
链表已经全部顺利遍历完成,oldFiber
链表还有节点的时候,删除剩下的oldFiber
节点。
第二次循环遍历
- 第二次循环遍历的两个作用,我们在文章当中已经全部展示了。
-
作用1:文章开头说的初次渲染。
-
作用2:第一次循环遍历顺利的时候,当
oldFiber
链表节点已经遍历完成后,newFiber
还有的时候,继续遍历newFiber
来创建fiber
,建立联系。
-
第三次循环遍历
- 上文从代码的角度聊了节点的移动策略,下面我们以dom的角度来表达移动diff的过程。
旧节点 a -> b -> c -> d -> e -> f (key为自己)
新节点 a -> c -> b -> e -> g (key为自己)
遍历新节点到c发现,b不能符合立马跳出第一次循环,将剩下的旧节点放到了map当中(key为item)。 {"b": b, "c": c, "d": d, "e": e, "f": f}, 遍历新节点c去map当中去找,找到了 placeIndex =2, 更新节节点不移动,删除map当中的c,遍历到b找到了,index < placeIndx -> 1 < 2 ,所以标记b为移动节点,同理e更新,g在map当中找不到,标记为增加。
遍历完之后要去更新dom了,这里要注意,先删除真实dom节点中要移动和删除的节点,删除之后真实dom变为 a -> c -> e,这时b移动的时候,索引为原来的mountIndex 为2,判断2上有没有元素,这里a -> c -> e 索引为2是e,所以执行parentDom.insertBefore() 将b插入到e之前,完成移动,执行到g新增的时候,索引为原来的mountIndex为4,判断4上有没有元素,这里a -> c -> b -> e,索引为4没有执行parentDom.appendChild(), 完成新增,最终结果 a -> c -> b -> e -> g,移动,更新完毕。
一点总结
我的两点理解,希望可以帮助到与我同样纠结在此的前端程序员们
diff
过程是一层一层的oldFiber
和newFiber
去diff。移动
不是dom
节点的平移,本质上还是复用,只不过不用重新document.createElement()
,利用fiber结构
上的引用stateNode
去将真实DOM,去insertBefore
或者appendChild
来完成的移动。
转载自:https://juejin.cn/post/7149818538155474951