第一篇不仅(虽然)排版可能有点烂而且(但)终于有研究稿了。最后,看完之后,你会有所收获。他会将当前组件与上次更新时该组件对应的Fiber节点进行比较,并根据比较结果生成一个新的Fiber节点。!为了防止概念上的混淆,这里强调一个DOM节点在某个时刻最多会有4个节点与之相关。-1.当前光纤。如果该DOM节点已经在页面上,则当前Fiber代表该DOM节点对应的Fiber节点。-2.进行中的纤维。如果本次更新要渲染DOM节点到页面,workInProgressFiber代表DOM节点对应的Fiber节点。-3.DOM节点本身。-4.JSX对象。即ClassComponent的render方法的返回结果,或者FunctionComponent的调用结果,JSX对象包含了描述DOM节点的信息。diff算法的本质是比较1和4,生成2。Diff的瓶颈以及React如何应对。diff操作本身也会带来性能损失。React文档中提到,即使是最前沿的算法,完全比较前后两棵树的算法复杂度也是O(n3),其中n是树中元素的个数。如果在React中使用此算法,显示1000个元素所执行的计算量将达到数十亿级。这个成本实在是太高了。因此,为了降低算法的复杂度,React的diff会预设三个限制条件:1.同级diff元素。如果一个DOM节点在两次更新之间跨越层次结构,React将不会尝试重用它。2.不同类型的元素会产生不同的树。如果元素从div变为p,React将销毁div及其后代,并创建p及其后代。3.读者可以使用keyprop来提示哪些子元素在不同的渲染下可以保持稳定。那么我们来看看Diff是如何实现的。我们可以从同级节点的个数把Diff分为两种类型:1.当newChild的类型为object,number,string时,表示同级只有一个节点-2.当类型newChild的是Array,同级有多个节点。单节点diff以Object类型为例,会进入这个函数reconcileSingleElement。这个函数会做如下的事情:让我们看看第二步判断一个DOM节点是否可以重用是如何实现的。详见前端高级面试题答案。从代码可以看出,React首先判断key是否相同,如果key相同则判断类型是否相同。只有当它们都相同时,一个DOM节点才能被重用。这里有一个需要注意的细节:1.当child!==null且key相同,类型不同时,执行deleteRemainingChildren标记删除child及其兄弟fibers。2.child!==null且key不同时,只删除child标签。例子:当前页面有3个li,我们要全部删除,然后插入一个p。由于本次更新只有一个p,属于单个节点的Diff会遵循上面介绍的代码逻辑。解释:在reconcileSingleElement中,遍历之前的3个fibers(对应的DOM为3个lis),找出更新后的p是否可以复用之前3个fibers之一的DOM。当key相同,类型不同时,说明我们找到了更新后的p对应的最后一个fiber,但是p和li的类型不同,不能复用。由于唯一的可能性不能再被重用,剩下的纤程就没有机会了,所以都需要标记为删除。当key不同时,只说明遍历的fiber不能被p重用,还有兄弟fiber还没有遍历过。所以只需将纤程标记为删除即可。Exercises:Exercises1:如果没有设置keyprop,默认key=null;,所以update前后的key是一样的,都是null,但是type在update之前是div,update之后是p更新。如果类型发生变化,则无法重复使用。Exercise2:key在update前后变化,不需要判断类型,不能重复使用。练习3:更新前后key没变,但类型变了,不能重用。练习4:更新前后key和type没有变化,可以重复使用。子元素发生变化,需要更新DOM的子元素。多节点diff同级多个节点的diff,必然属于以下3种情况中的一种或多种。情况一:节点更新情况二:节点增加或减少情况三:节点位置变化这里要注意diff算法不能使用双指针优化我们在做数组相关的算法题的时候,经常使用双指针从头到尾遍历同时使用数组来提高效率,但这里不是。更新后的JSX对象newChildren虽然是数组形式,但是和newChildren中的各个组件相比,与当前fiber同级的Fiber节点是一个由兄弟指针链接组成的单链表。即newChildren[0]与fiber比较,newChildren[1]与fiber.sibling比较。所以不能使用双指针优化。基于以上原因,Diff算法的整体逻辑会经过两轮遍历:1.第一轮遍历:处理更新的节点。2、第二轮遍历:处理剩余未更新的节点。第一轮遍历:第一轮遍历的步骤如下:令i=0,遍历newChildren,比较newChildren[i]和oldFiber,判断DOM节点是否可用reuse。如果可以复用,i++,继续比较newChildren[i]和oldFiber.sibling,如果可以复用继续遍历。如果不可重用,则立即跳出整个遍历,第一轮遍历结束。如果newChildren遍历完毕(即i===newChildren.length-1)或者oldFiber遍历完毕(即oldFiber.sibling===null),则跳出遍历,第一轮遍历结束。以上3的遍历此时跳出newChildren还没有遍历,oldFiber也没有遍历。上面的例子:前2个节点可以复用,遍历key===2的节点发现类型变了,不能复用,所以跳出遍历。此时oldFiber留下了key===2没有遍历,newChildren留下了key===2和key===3没有遍历。以上4次跳转的遍历可能由newChildren、oldFiber或两者同时完成。上面的例子:有了第一轮遍历的结果,我们开始第二轮遍历。第一轮遍历:(4例)-1.同时遍历NewChildren和oldFiber。这是最理想的情况:只更新组件。至此Diff结束。-2.newChildren还没遍历完,oldFiber已经遍历了已有的DOM节点并复用了。这个时候有新节点added,说明本次更新有新节点插入。我们只需要遍历剩下的newChildren,依次生成workInProgressfibermarksPlacement。-3.NewChildren已经遍历完了,oldFiber没有遍历完,说明这次更新的节点数比上一次少,删除了一些节点。所以需要遍历剩下的oldFiber,依次标记删除。-4.newChildren和oldFiber都没有遍历过,也就是说在这次更新的过程中,有些节点的位置发生了变化。改变位置需要我们处理移动节点。由于部分节点改变了位置,我们不能再使用位置索引i来比较前后节点。那么我们如何在两次更新中匹配同一个节点呢?我们需要使用密钥。为了快速找到key对应的oldFiber,我们将所有未处理的oldFiber存储在一个以key为key,oldFiber为value的Map中。接下来,遍历剩余的newChildren。通过newChildren[i].key,可以在existingChildren中找到相同key的oldFiber标记节点在移动!既然我们的目标是找到移动的节点,那么我们就需要明确:节点是否移动的依据是什么?作为参考?我们的引用是:oldFiber中最后一个可重用节点的位置索引(用变量lastPlacedIndex表示)。由于本次更新的节点是按照newChildren的顺序排列的。在遍历newChildren的过程中,所遍历的每个可重用节点必须是当前遍历的所有可重用节点中最右边的一个,即必须在后面本次更新的lastPlacedIndex对应的可重用节点的位置。那么我们只需要比较遍历的可重用节点是否在上次更新时lastPlacedIndex对应的oldFiber后面,就可以知道这两个节点的相对位置在两次更新中是否发生了变化。我们使用变量oldIndex来表示遍历的可重用节点在oldFiber中的位置索引。如果oldIndex
