深入React Diff算法

fiber上的updateQueue经过React的一番核算之后,这个fiber现已有了新的状况,也便是state,关于类组件来说,state是在render函数里被运用的,已然现已得到了新的state,那么当务之急是履行一次render,得到持有新state的ReactElement。

假定render一次之后得到了很多的ReactElement,而这些ReactElement之中若只有少量需求更新的节点,那么明显不能悉数去更新它们,此刻就需求有一个diff进程来决议哪些节点是真实需求更新的。

源码结构

咱们以类组件为例,state的核算发生在类组件对应的fiber节点beginWork中的updateClassInstance函数中,在状况核算完毕之后,紧跟着便是去调finishClassComponent履行diff、打上effectTag(即新版本的flag)。

打上effectTag能够标识这个fiber发生了怎样的改变,例如:新增(Placement)、更新(Update)、删去(Deletion),这些被打上flag的fiber会在complete阶段被收集起来,形成一个effectList链表,只包括这些需求操作的fiber,最后在commit阶段被更新掉。

function updateClassComponent(
   current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {
   ...
   // 核算状况
   shouldUpdate = updateClassInstance(
     current,
     workInProgress,
     Component,
     nextProps,
     renderLanes,
   );
   ...
   // 履行render,进入diff,为fiber打上effectTag
   const nextUnitOfWork = finishClassComponent(
     current, 
     workInProgress,
     Component,
     shouldUpdate,
     hasContext,
     renderLanes,
     );
     return nextUnitOfWork;
 }

相关参考视频解说:进入学习

finishClassComponent函数中,调用reconcileChildFibers去做diff,而reconcileChildFibers实践上便是ChildReconciler,这是diff的中心函数,
该函数针对组件render生成的新节点的类型,调用不同的函数进行处理。

function ChildReconciler(shouldTrackSideEffects) {
   ...
   function reconcileSingleElement(
      returnFiber: Fiber,      currentFirstChild: Fiber | null,      element: ReactElement,      lanes: Lanes,   ): Fiber {
     // 单节点diff
   }
  function reconcileChildrenArray(
     returnFiber: Fiber,     currentFirstChild: Fiber | null,     newChildren: Array<*>,     lanes: Lanes,  ): Fiber | null {
    // 多节点diff
  }
   ...
   function reconcileChildFibers(
     returnFiber: Fiber,     currentFirstChild: Fiber | null,     newChild: any, lanes: Lanes,   ): Fiber | null {
     const isObject = typeof newChild === 'object' && newChild !== null;
     if (isObject) {
       // 处理单节点
       switch (newChild.$$typeof) {
         case REACT_ELEMENT_TYPE:
           return placeSingleChild(
             reconcileSingleElement(
             returnFiber,
             currentFirstChild,
             newChild,
             lanes,
           ),
        );
        case REACT_PORTAL_TYPE:
        ...
        case REACT_LAZY_TYPE:
        ...
      }
    }
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // 处理文本节点
    }
    if (isArray(newChild)) {
      // 处理多节点
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
     );
   }
   ...
 }
 return reconcileChildFibers;
}

Diff的主体

关于Diff的参与者,在reconcileChildren函数的入参中能够看出

workInProgress.child = reconcileChildFibers(
 workInProgress,
 current.child,
 nextChildren,
 renderLanes,
 );
  • workInProgress:作为父节点传入,新生成的第一个fiber的return会被指向它。
  • current.child:旧fiber节点,diff生成新fiber节点时会用新生成的ReactElement和它作比较。
  • nextChildren:新生成的ReactElement,会以它为标准生成新的fiber节点。
  • renderLanes:本次的烘托优先级,终究会被挂载到新fiber的lanes特点上。
    能够看出,diff的两个主体是:oldFiber(current.child)和newChildren(nextChildren,新的ReactElement),它们是两个不一样的数据结构。

比如现在有组件,它核算完新的状况之后,要根据这两个东西去做diff,分别是现有fiber树中(current树)对应fiber的一切子fiber节点的render函数的履行成果,即那些ReactElements

对应fiber的一切子fiber节点:oldFiber

 current树中
 <Example/> fiber
     |
     |
     A --sibling---> B --sibling---> C

的render函数的履行成果,newChildren

 current fiber 对应的组件render的成果
 [
    {$$typeof: Symbol(react.element), type: "div", key: "A" },
    {$$typeof: Symbol(react.element), type: "div", key: "B" }, 
    {$$typeof: Symbol(react.element), type: "div", key: "B" },
 ]

Diff的根本原则

关于新旧两种结构来说,场景有节点本身更新、节点增删、节点移动三种状况。面临杂乱的状况,即使最前沿的算法杂乱度也极高。面临这种状况,React以如下战略应对:

  • 即使两个元素的子树彻底一样,但前后的父级元素不同,依照规则div元素及其子树会彻底销毁,并重建一个p元素及其子树,不会测验复用子树。
<div>
 <span>a</span>
 <span>b</span>
</div><p>
 <span>a</span>
 <span>b</span>
</p>
  • 运用tag(标签名)和 key辨认节点,区分出前后的节点是否改变,以达到尽量复用无改变的节点。
<p key="a">aa</p>
<h1 key="b">bb</h1><h1 key="b">bb</h1>
<p key="a">aa</p>

由于tag 和 key的存在,所以React能够知道这两个节点仅仅方位发生了改变。

场景

上面提到diff算法应对三种场景:节点更新、节点增删、节点移动,但一个fiber的子元素有或许是单节点,也有或许是多节点。所以依据这两类节点能够再细分为:

  • 单节点更新、单节点增删。
  • 多节点更新、多节点增删、多节点移动。

什么是节点的更新呢?关于DOM节点来说,在前后的节点类型(tag)和key都相同的状况下,节点的特点发生了改变,是节点更新。若前后的节点tag或许key不相同,Diff算法会认为新节点和旧节点毫无关系。

以下比如中,key为b的新节点的className发生了改变,是节点更新。

旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'b'}>bb</div>

以下比如中,新节点的className虽然有改变,但key也改变了,不归于节点更新

旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'bbb'}>bb</div>

以下比如中,新节点的className虽然有改变,但tag也改变了,不归于节点更新

旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<p className={'bcd'} key={'b'}>bb</p>

下面来分开叙说一下单节点和多节点它们各自的更新战略。

单节点

若组件产出的元素是如下的类型:

<div key="a">aa</div>

那么它终究产出的ReactElement为下面这样(省掉了一些与diff相关度不大的特点)

{
   $$typeof: Symbol(react.element), type: "div", key: "a" 
   ...
}

单节点指newChildren为单一节点,但是oldFiber的数量不一定,所以实践有如下三种场景:

为了下降理解本钱,咱们用简化的节点模型来阐明问题,字母代表key。

  • 单个旧节点
旧: A
新: A
  • 多个旧节点
旧: A - B - C
新: B
  • 没有旧节点
旧: --
新: A

关于单节点的diff,其实就只有更新操作,不会涉及位移和方位的改变,单节点的更新会调用reconcileSingleElement函数处理。该函数中对以上三种场景都做了覆盖。但实践上面的状况关于React来说仅仅两种,oldFiber链是否为空。因而,在完成上也只处理了这两种状况。

oldFiber链不为空

遍历它们,找到key相同的节点,然后删去剩余的oldFiber节点,再用匹配的oldFiber,newChildren中新节点的props来生成新的fiber节点。

   function reconcileSingleElement(
     returnFiber: Fiber,     currentFirstChild: Fiber | null,     element: ReactElement,     lanes: Lanes   ): Fiber {
     const key = element.key;
     let child = currentFirstChild;
     while (child !== null) {
        if (child.key === key) {
          switch (child.tag) {
            case Fragment:
            ...
            case Block:
            ...
            default: {
              if (child.elementType === element.type) {
                 // 先删去剩余的oldFiber节点
                deleteRemainingChildren(returnFiber, child.sibling);
                // 根据oldFiber节点和新节点的props新建新的fiber节点
                const existing = useFiber(child, element.props);
                existing.ref = coerceRef(returnFiber, child, element);
                existing.return = returnFiber; return existing;
              }
              break;
            }
         }
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // 没匹配到阐明新的fiber节点无法从oldFiber节点新建
        // 删去掉一切oldFiber节点
        deleteChild(returnFiber, child);
     }
     child = child.sibling;
   }
 ...
 }

oldFiber链为空

关于没有oldFiber节点的状况,只能新建newFiber节点。逻辑不杂乱。

   function reconcileSingleElement(
     returnFiber: Fiber,     currentFirstChild: Fiber | null,     element: ReactElement,     lanes: Lanes   ): Fiber {
     const key = element.key;
     let child = currentFirstChild;
     while (child !== null) {
        // oldFiber链非空的处理
        ...
     } if (element.type === REACT_FRAGMENT_TYPE) {
        // 处理Fragment类型的节点
        ... 
     } else {
        // 用产生的ReactElement新建一个fiber节点
        const created = createFiberFromElement(element, returnFiber.mode, lanes);
        created.ref = coerceRef(returnFiber, currentFirstChild, element);
        created.return = returnFiber;
        return created;
     }
   }

单节点的更新便是这样的处理,真实比较杂乱的状况是多节点的diff。由于它涉及到节点的增删和位移。

多节点

若组件终究产出的DOM元素是如下这样:

<div key="a">aa</div>
<div key="b">bb</div>
<div key="c">cc</div>
<div key="d">dd</div>

那么终究的newChildren为下面这样(省掉了一些与diff相关度不大的特点)

[
 {$$typeof: Symbol(react.element), type: "div", key: "a" },
 {$$typeof: Symbol(react.element), type: "div", key: "b" },
 {$$typeof: Symbol(react.element), type: "div", key: "c" },
 {$$typeof: Symbol(react.element), type: "div", key: "d" }
]

多节点的改变有以下四种或许性。

  • 节点更新
旧: A - B - C
新: `A - B - C`
  • 新增节点
旧: A - B - C
新: A - B - C - `D - E`
  • 删去节点
旧: A - B - C - `D - E`
新: A - B - C
  • 节点移动
旧: A - B - C - D - E
新: A - B - `D - C - E`

多节点的状况一定是归于这四种状况的任意组合,这种状况会调用reconcileChildrenArray进行diff。依照以上四种状况,它会以newChildren为主体进行最多三轮遍历,但这三轮遍历并不是相互独立的,事实上只有第一轮是从头开始的,之后的每一轮都是上轮完毕的断点持续。实践上在平时的实践中,节点本身的更新是最多的,所以Diff算法会优先处理更新的节点。因而四轮遍历又能够依照场景分为两部分:

第一轮是针对节点本身特点更新,剩余的两轮顺次处理节点的新增、移动,而重点又在移动节点的处理上,所以本文会着重解说节点更新和节点移动的处理,对删去和新增简单带过。

节点更新

第一轮从头开始遍历newChildren,会逐个与oldFiber链中的节点进行比较,判别节点的key或许tag是否有改变。

  • 没变则从oldFiber节点clone一个props被更新的fiber节点,新的props来自newChildren中的新节点,这样就完成了节点更新。
  • 有改变阐明不满足复用条件,当即中止遍历进入下边的遍历。Diff算法的杂乱度也由于这个操作大幅下降。
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
   ...
   // 更新节点,关于DOM节点来说,updateSlot内部会判别
   // key 和 tag。任意一个不同,则回来null
   const newFiber = updateSlot( returnFiber,
     oldFiber,
     newChildren[newIdx],
     lanes,
   );
   // newFiber为null则阐明当时的节点不是更新的场景,间断这一轮循环
   if (newFiber === null) {
     if (oldFiber === null) {
        oldFiber = nextOldFiber;
     }
     break;
   }
    ...
 }

咱们来看一个比如,假定新旧的节点如下:

旧: A – B – C - D – E
新: A – B – D - C

在本轮遍历中,会遍历A - B - D - C。A和B都是key没变的节点,能够直接复用,但当遍历到D时,发现key改变了,跳出当时遍历。比如中A 和 B是本身发生更新的节点,后边的D 和 C咱们看到它的方位相关于oldFiber链发生了改变,会往下走到处理移动节点的循环中。

关于移动节点的参照物

为了方便阐明,把保留在原位的节点称为固定节点。经过这次循环的处理,能够看出固定节点是A 和 B。在newChildren中,最靠右的固定节点的方位至关重要,关于后续的移动节点的处理来说,它的意义是供给参考方位。所以,每当处理到最后一个固定节点时,要记住此刻它的方位,这个方位便是lastPlacedIndex。关键代码如下:

let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
 ...
 // 跳出逻辑
 ...
 // 如果不跳出,记载最新的固定节点的方位
 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
 ...}

placeChild办法实践上是移动节点的办法,但当节点无需移动的时分,会回来当时节点的方位,关于固定节点来说,由于无需移动,所以回来的便是固定节点的index。

节点删去

咱们没有提到对删去节点的处理,实践上删去节点比较简单。

旧: A – B – C – D - E 新: A – B – C

由于遍历的是newChildren,当它遍历完毕,但oldFiber链还没有遍历完,那么阐明剩余的节点都要被删去。直接在oldFiber节点上标记Deletion的effectTag来完成删去。

if (newIdx === newChildren.length) {
   // 新子节点遍历完,阐明剩余的oldFiber都是没用的了,能够删去
   deleteRemainingChildren(returnFiber, oldFiber);
   return resultingFirstChild;
}

deleteRemainingChildren调用了deleteChild,值得注意的是,删去不仅仅是标记了effectTag为Deletion,还会将这个被删去的fiber节点添加到父级的effectList中。

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
   ...
   const last = returnFiber.lastEffect;
   // 将要删去的child添加到父级fiber的effectList中,并添加上effectTag为删去
   if (last !== null) {
     last.nextEffect = childToDelete;
     returnFiber.lastEffect = childToDelete;
   } else {
     returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
   }
   childToDelete.nextEffect = null;
   childToDelete.effectTag = Deletion;
}

节点新增

新增节点的场景也很好理解,当oldFiber链遍历完,但newChildren还没遍历完,那么余下的节点都归于新插入的节点,会新建fiber节点并以sibling为指针连成fiber链。

旧: A – B – C

新: A – B – C – D - E

插入的逻辑(省掉了相关度不高的代码)

if (oldFiber === null) {
 // 旧的遍历完了,意味着剩余的都是新增的了
 for (; newIdx < newChildren.length; newIdx++) { // 首要创立newFiber
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    ...
    // 再将newFiber连接成以sibling为指针的单向链表
    if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
    } else {
        previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
  return resultingFirstChild;
}

节点移动

节点的移动是如下场景:

旧 A – B – C - D - E - F

新 A – B – D - C - E

经过第一轮遍历的处理,固定节点为A B,最新的固定节点的方位(lastPlacedIndex)为1(B的方位)。此刻oldFiber链中还剩C – D – E – F,newChildren中还剩D – C – E。

接下来的逻辑关于方位不一样的节点,它自己会先更新再移动。由于此刻剩余的节点方位变了,更新又要复用oldFiber节点,所以为了在更新时方便查找,会将剩余的oldFiber节点放入一个以key为键,值为oldFiber节点的map中。称为existingChildren

由于newChildren 和 oldFiber节点都没遍历完,阐明需求移动方位。此刻需求清晰一点,便是这些节点都在最新的固定节点的右边

移动的逻辑是:newChildren中剩余的节点,都是不确认要不要移动的,遍历它们,每一个都去看看这个节点在oldFiber链中的方位(旧方位),遍历到的节点有它在newChildren中的方位(新方位):

如果旧方位在lastPlacedIndex的右边,阐明这个节点方位不变。

原因是旧方位在lastPlacedIndex的右边,而新节点的方位也在它的右边,所以它的方位没改变。由于方位不变,所以它成了固定节点,把lastPlacedIndex更新成新方位。

如果旧方位在lastPlacedIndex的左边,当时这个节点的方位要往右挪。

原因是旧方位在lastPlacedIndex的左边,新方位却在lastPlacedIndex的右边,所以它要往右挪,但它不是固定节点。此刻无需更新lastPlacedIndex。

咱们来用上边的比如过一下这部分逻辑。

旧 A – B – C - D - E - F 新 A – B – D - C - E

方位固定部分 A – B,最右侧的固定节点为B,lastPlacedIndex为1。这时剩余oldFiber链为C – D – E – F,existingChildren为

{
   C: '节点C',
   D: '节点D',
   E: '节点E',
   F: '节点F'
}

newChildren的剩余部分D – C – E持续遍历。

首要遍历到D,D在oldFiber链中(A – B – C – D – E)的方位为3

3 > 1,oldFiber中D的方位在B的右边,newChildren中也是如此,所以D的方位不动,此刻最新的固定节点变成了D,更新lastPlacedIndex为3。并从existingChildren中删去D,

{
   C: '节点C',
   E: '节点E',
   F: '节点F'
}

再遍历到C,C在oldFiber链中(A – B – C – D – E)的索引为2

2 < 3,C本来在最新固定节点(D)的左边,newChildren中C在D的右边,所以要给它移动到右边。并从existingChildren中删去C。

{
   E: '节点E',
   F: '节点F'
}

再遍历到E,E在oldFiber链中(A – B – C – D – E)的方位为4

4 > 3,oldFiber链中E方位在D的方位的右边,新方位中也是如此,所以E的方位不动,此刻最新的固定节点变成了E,更新lastPlacedIndex为4。并从existingChildren中删去E,

{
   F: '节点F'
}

这个时分newChildren都处理完了,针对移动节点的遍历完毕。此刻还剩一个F节点,是在oldFiber链中的,由于newChildren都处理完了,所以将它删去即可。

existingChildren.forEach(child => deleteChild(returnFiber, child));

能够看到,节点的移动是以最右侧的固定节点方位作为参照的。这些固定节点是指方位未发生改变的节点。每次对比节点是否需求移动之后,及时更新固定节点非常重要。

源码

了解了上边的多节点diff原理后,将上边的关键点匹配到源码上更方便能进一步理解。下面放出带有具体注释的源码。

 function reconcileChildrenArray(
    returnFiber: Fiber,    currentFirstChild: Fiber | null,    newChildren: Array<*>,    lanes: Lanes,
): Fiber | null {
    /* * returnFiber:currentFirstChild的父级fiber节点       * currentFirstChild:当时履行更新使命的WIP(fiber)节点       * newChildren:组件的render办法烘托出的新的ReactElement节点       * lanes:优先级相关    * */
    // resultingFirstChild是diff之后的新fiber链表的第一个fiber。
    let resultingFirstChild: Fiber | null = null;
    // resultingFirstChild是新链表的第一个fiber。
    // previousNewFiber用来将后续的新fiber接到第一个fiber之后
    let previousNewFiber: Fiber | null = null;
    // oldFiber节点,新的child节点会和它进行比较
    let oldFiber = currentFirstChild;
    // 存储固定节点的方位
    let lastPlacedIndex = 0;
    // 存储遍历到的新节点的索引
    let newIdx = 0;
    // 记载目前遍历到的oldFiber的下一个节点
    let nextOldFiber = null;
    // 该轮遍历来处理节点更新,依据节点是否可复用来决议是否中止遍历
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
        // newChildren遍历完了,oldFiber链没有遍历完,此刻需求中止遍历
        if (oldFiber.index > newIdx) {
            nextOldFiber = oldFiber; oldFiber = null;
        } else {
            // 用nextOldFiber存储当时遍历到的oldFiber的下一个节点
            nextOldFiber = oldFiber.sibling;
        }
        // 生成新的节点,判别key与tag是否相同就在updateSlot中
        // 对DOM类型的元素来说,key 和 tag都相同才会复用oldFiber
        // 并回来出去,否则回来null
        const newFiber = updateSlot(
            returnFiber,
            oldFiber,
            newChildren[newIdx],
            lanes,
        );
        // newFiber为 null阐明 key 或 tag 不同,节点不可复用,中止遍历
        if (newFiber === null) {
            if (oldFiber === null) {
            // oldFiber 为null阐明oldFiber此刻也遍历完了
            // 是以下场景,D为新增节点
            // 旧 A - B - C 
            // 新 A - B - C - D oldFiber = nextOldFiber;
            }
            break;
        }
        if (shouldTrackSideEffects) {
            // shouldTrackSideEffects 为true表示是更新进程
            if (oldFiber && newFiber.alternate === null) {
                // newFiber.alternate 等同于 oldFiber.alternate 
                // oldFiber为WIP节点,它的alternate 便是 current节点
                // oldFiber存在,而且经过更新后的新fiber节点它还没有current节点,
                // 阐明更新后展现在屏幕上不会有current节点,而更新后WIP
                // 节点会称为current节点,所以需求删去已有的WIP节点
                deleteChild(returnFiber, oldFiber);
                }
            }
            // 记载固定节点的方位
            lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
            // 将新fiber连接成以sibling为指针的单向链表
            if (previousNewFiber === null) {
                resultingFirstChild = newFiber;
            } else {
                previousNewFiber.sibling = newFiber;
            }
            previousNewFiber = newFiber;
            // 将oldFiber节点指向下一个,与newChildren的遍历同步移动
            oldFiber = nextOldFiber;
         }
        // 处理节点删去。新子节点遍历完,阐明剩余的oldFiber都是没用的了,能够删去.
        if (newIdx === newChildren.length) {
            // newChildren遍历完毕,删去掉oldFiber链中的剩余的节点
            deleteRemainingChildren(returnFiber, oldFiber);
            return resultingFirstChild;
        }
        // 处理新增节点。旧的遍历完了,能复用的都复用了,所以意味着新的都是新插入的了
        if (oldFiber === null) {
            for (; newIdx < newChildren.length; newIdx++) {
                // 根据新生成的ReactElement创立新的Fiber节点
                const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
                if (newFiber === null) {
                    continue;
                }
                // 记载固定节点的方位lastPlacedIndex
                lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); 
                // 将新生成的fiber节点连接成以sibling为指针的单向链表
                if (previousNewFiber === null) {
                    resultingFirstChild = newFiber;
                } else {
                    previousNewFiber.sibling = newFiber; 
                }
                previousNewFiber = newFiber;
            }
            return resultingFirstChild;
        }
        // 履行到这是都没遍历完的状况,把剩余的旧子节点放入一个以key为键,值为oldFiber节点的map中
        // 这样在根据oldFiber节点新建新的fiber节点时,能够经过key快速地找出oldFiber
        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
        // 节点移动
        for (; newIdx < newChildren.length; newIdx++) {
            // 根据map中的oldFiber节点来创立新fiber
            const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, ); 
            if (newFiber !== null) {
                if (shouldTrackSideEffects) {
                    if (newFiber.alternate !== null) {
                        // 由于newChildren中剩余的节点有或许和oldFiber节点一样,仅仅方位换了,
                        // 但也有或许是是新增的.
                        // 如果newFiber的alternate不为空,则阐明newFiber不是新增的。
                        // 也就阐明着它是根据map中的oldFiber节点新建的,意味着oldFiber现已被运用了,所以需
                        // 要从map中删去oldFiber
                        existingChildren.delete(
                            newFiber.key === null ? newIdx : newFiber.key,
                        );
                     }
                  }
                 // 移动节点,多节点diff的中心,这里真实会完成节点的移动
                 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
                 // 将新fiber连接成以sibling为指针的单向链表
                if (previousNewFiber === null) {
                    resultingFirstChild = newFiber;
                } else {
                    previousNewFiber.sibling = newFiber; 
                }
                previousNewFiber = newFiber;
            }
         }
        if (shouldTrackSideEffects) {
           // 此刻newChildren遍历完了,该移动的都移动了,那么删去剩余的oldFiber
           existingChildren.forEach(child => deleteChild(returnFiber, child));
        }
        return resultingFirstChild;
 }

总结

Diff算法经过key和tag来对节点进行取舍,可直接将杂乱的比对拦截掉,然后降级成节点的移动和增删这样比较简单的操作。对oldFiber和新的ReactElement节点的比对,将会生成新的fiber节点,同时标记上effectTag,这些fiber会被连到workInProgress树中,作为新的WIP节点。树的结构因而被一点点地确认,而新的workInProgress节点也根本定型。这意味着,在diff过后,workInProgress节点的beginWork节点就完成了。接下来会进入completeWork阶段。