当前位置: 首页 > 科技观察

从源码层面了解React是如何做Diff的

时间:2023-03-13 22:50:49 科技观察

大家好,我是前端西瓜哥。今天就带大家分析一下React的源码,了解一下单节点diff和多节点diff的具体实现。React的版本是18.2.0reconcileChildFibersReact的节点比较逻辑是在reconcileChildFibers方法中实现的。reconcileChildFibers是定义在ChildReconciler方法内部的一个方法,通过调用ChildReconciler方法并传入一个shouldTrackSideEffects参数返回。这样做是为了根据不同的使用场景产生不同的效果。因为更新和挂载组件的过程是不同的。比如挂载会执行挂载的生命周期函数,而更新则不会。//reconcileChildFibers与内部方法同名reconcileChildFibers的核心实现:functionreconcileChildFibers(return,currentFirstChild,newChild,lanes,){//newChild可以是数组也可以是对象//如果是数组,它的$$typeof是未定义的switch(newChild.$$typeof){caseREACT_ELEMENT_TYPE://单节点diffreturnplaceSingleChild(reconcileSingleElement(returnFiber,currentFirstChild,newChild,lanes,),);//...}//多节点diffif(isArray(newChild)){returnreconcileChildrenArray(returnFiber,currentFirstChild,newChild,lanes,);}}newChild在组件渲染时获取ReactElement,通过访问组件的props.children获取。如果newChild是对象(不是数组),它将调用reconcileSingleElement(在普通元素的情况下)来比较单个节点。如果是数组,会调用reconcileChildrenArray进行多节点diff。update和mount的逻辑有点不同,后面会结合“update”场景进行说明。单节点diff首先看单节点diff。需要注意的是,这里的“单节点”是指新生成的ReactElement是单个的。只要新节点是数组就不是单节点,即使数组长度只有1。另外,旧节点可能有兄弟节点(sibling不为空)。fiber对象通过链表表示节点之间的关系,它的sibling指向它的下一个兄弟节点,index表示它在兄弟节点中的位置。ReactElement是对象或数组的形式,由React.createElement()生成。单节点diff对应reconcileSingleElement方法,其核心实现是:让孩子=currentFirstChild;while(child!==null){if(child.key===key){constelementType=element.type;//key相同,类型相同(比如old和new都是div类型)//then"update"Logicalif(child.elementType===elementType){//【分支1】//标记并删除旧节点之后的所有兄弟deleteRemainingChildren(returnFiber,child.sibling);//创建WorkInProgress,它是原始fiber的替代品constexisting=useFiber(child,element.props.children);existing.return=returnFiber;返回现有的;}else{//[分支2]deleteRemainingChildren(returnFiber,child);休息;}}//当前节点键不匹配,标记为待删除else{//[Branch3]deleteChild(returnFiber,child);}//取下一个兄弟节点继续比较child=child.sibling;}//执行到这里表示没有找到可重用的节点,需要创建一个fiberconstcreated=createFiberFromElement(element,returnFiber.mode,lanes);created.return=returnFiber;returncreated;}currentFirstChild是update之前的节点,它存储在一个链表中,它的sibling指向它的下一个有很多分支的兄弟节点。下面我们详细分析一下。分支1:密钥相同,类型相同。当发现key相同时,React会尝试重用该组件。如果没有设置新旧节点的键,它们将被设置为空。如果新旧节点的key都为null,则认为它们相等。另外需要判断新旧类型是否相同(比如都是div),因为类型不同,不能复用。如果全部满足,旧fiber后面的所有兄弟节点将被标记为删除,具体调用deleteRemainingChildren()方法,该方法会将指定的子fiber及其之后的所有兄弟节点添加到parentfiber的deletions数组中,作为删除标记。在后续的commit阶段,会进行正式的删除,并会执行一些调用生命周期函数等逻辑。useFiber()将创建旧光纤的替代品,将其更新为光纤的替代属性,最后这个useFiber返回这个替代品。然后直接返回结束这个方法。分支2:相同的密钥但不同类型不能重复使用。如果类型不同但key相同,React会认为没有匹配的可重用节点。直接将剩余的兄弟节点标记为删除,然后结束循环。分支3:key不匹配key不同,使用deleteChild()方法将当前fiber节点标记为待删除,取出下一个兄弟节点与新节点比较,继续循环直到一个的分支是匹配的。这是三个分支。如果能到达循环的末尾,说明没有找到可复用的fiber,会根据ReactElement调用createFiberFromElement()方法创建一个新的fiber,然后返回。外部会拿到这个fiber,然后调用placeSingleChild()来标记要更新的tag。reconcileChildrenArray,然后是多节点差异。对应ReactElement为数组的场景,该场景的算法实现要复杂很多。多节点diff对应reconcileChildrenArray方法。因为算法比较复杂,我们就不直接贴比较完整的代码了,而是分成几个阶段来一点点讲解。多节点diff分为4个阶段,下面会详细介绍。Phase1:从左到右同时遍历oldfiber和element的指针,一起从左到右。指针分别是nextFiber和newIdx,从左到右遍历。遍历过程中发生的逻辑如下:当一个指针完成,即nextFiber变为null或者newIdx大于newChildren.length时,循环结束。如果key不同则遍历结束(源码中updateSlot()返回null赋值给newFiber,然后跳出循环)。如果key相同,但类型不同,说明旧节点不可用,标记为“删除”,继续遍历。key是一样的,type也是一样的,节点都是复用的。对于普通元素类型,最终会调用updateElement方法。updateElement方法会判断fiber和element的类型是否相同。如果它们相同,则将生成一个workInProcess(替代)fiber用于fiber的alternate返回,否则将创建一个新的fiber并返回。他们将携带新的pendingProps属性。functionreconcileChildrenArray(returnFiber,currentFirstChild,//oldfibernewChildren,//新节点数组通道,){letoldFiber=currentFirstChild;让lastPlacedIndex=0;让newIdx=0;让nextOldFiber=null;从左到右遍历比较更新for(;oldFiber!==null&&newIdxnewIdx){//老纤维比新元素多nextOldFiber=oldFiber;旧纤维=空;}else{nextOldFiber=oldFiber.sibling;}//更新节点(或者生成一个新的要插入的节点)//该方法会判断键是否相等,如果不相等则返回null。constnewFiber=updateSlot(returnFiber,oldFiber,newChildren[newIdx],车道,);//如果当前新旧节点不匹配,则跳出循环if(newFiber===null){if(oldFiber===null){oldFiber=nextOldFiber;}休息;}if(shouldTrackSideEffects){if(oldFiber&&newFiber.alternate===null){//newFiber不是基于oldFiber的alternate创建的//表示需要销毁oldFiber并标记为“已删除”deleteChild(returnFiber,oldFiber);}}//标记“放置”lastPlacedIndex=placeChild(newFiber,lastPlacedIndex,newIdx);}}阶段2:新节点遍历完成后跳出循环,检查新节点数组是否遍历完成(newIdx是否等于newChildren.length)。如果是,将旧节点中剩余的节点全部编辑为“删除”,直接结束整个函数。functionreconcileChildrenArray(returnFiber,currentFirstChild,//oldfibernewChildren,//newnodearraylanes,){//[1]从左到右遍历比较更新//...//[2]如果是新节点遍历完成,将旧节点的所有剩余节点标记为已删除if(newIdx===newChildren.length){//我们已经到达新子节点的末尾。我们可以删除其余的。deleteRemainingChildren(returnFiber,oldFiber);返回resultingFirstChild;}}第三阶段:旧节点已经遍历完,新节点还没有遍历。如果旧节点已经遍历完,但新节点还没有遍历,则新节点中剩余的节点将根据元素构造为纤维。functionreconcileChildrenArray(returnFiber,currentFirstChild,//oldfibernewChildren,//newnodearraylanes,){//[1]从左到右遍历比较更新//...//[2]如果是新节点遍历完成,将旧节点的剩余节点全部标记为已删除//...//【3】如果旧节点已经遍历完,但是新节点还没有遍历,则根据剩余节点生成新的fibernewnodesif(oldFiber===null){for(;newIdx