探索React源码:React Diff

本篇文章我们来了解一下Diff算法的实现过程。

相关概念

React中的各种节点

假设当前存在一个DOM节点,触发了一次更新,那么在协调的过程中,会有四种节点和该节点相关联:

  1. DOM节点本身。
  2. workInProgress fiber,更新过程中产生的workInProgress Tree中的fiber节点(即current
    fiber.alternate)。
  3. current fiber,在页面中已经渲染了的DOM节点对应的fiber节点(即workInProgress fiber.alternate,也就是上一次更新中产生的workInProgress fiber)。
  4. ReactElement,更新过程中,ClassComponent的render方法或FunctionComponent的调用结果。ReactElement中包含描述DOM节点的信息。

Diff算法可以理解为更新的过程中产生了新的ReactElement, 在f协调过程中将ReactElement与对应的current fiber进行对比后, 产生了workInProgress fiber。

双缓冲机制

双缓存 (opens new window)机制是一种在内存中构建并直接替换的技术。在render的过程中就使用了这种技术。

在React中同时存在着两棵fiber tree。一棵是在屏幕上显示的dom对应的fiber tree,称为current fiber tree,而还有一棵是当触发新的更新任务时,React在内存中构建的fiber tree,称为workInProgress fiber tree

current fiber treeworkInProgress fiber tree中的fiber节点通过alternate属性进行连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点中也存在current属性,利用current属性在不同fiber tree的根节点之间进行切换的操作,就能够完成current fiber tree与workInProgress fiber tree之间的切换。

在协调阶段,React利用diff算法,将产生update的React elementcurrent fiber tree中的节点进行比较,并最终在内存中生成workInProgress fiber tree。此时Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。

effectTag

effectTag正是用于保存要执行DOM操作的具体类型的。
effectTag通过二进制表示:

//...
// 意味着该Fiber节点对应的DOM节点需要插入到页面中。
export const Placement = /*                    */ 0b000000000000010;
//意味着该Fiber节点需要更新。
export const Update = /*                       */ 0b000000000000100;
export const PlacementAndUpdate = /*           */ 0b000000000000110;
//意味着该Fiber节点对应的DOM节点需要从页面中删除。
export const Deletion = /*                     */ 0b000000000001000;
//...

Diff的瓶颈

在React官网的协调一文提到:

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

此算法有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n 3 ),其中 n 是树中元素的数量。

React如何解决Diff的瓶颈

React 在以下三个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 只对同级元素进行Diff;
  2. 两个不同类型的元素会产生出不同的树;
  3. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

React Diff 的实现

React Diff的入口函数为reconcileChildrenreconcileChildren内部会通过current === null 区分当前fiber节点是mount还是update,再分别执行不同的工作:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

mountChildFibersreconcileChildFibers的都是通过ChildReconciler生成的。

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

他们的不同点在于shouldTrackSideEffects的参数的值不一样,shouldTrackSideEffects为true时会为生成的fiber节点收集effectTag属性,反之不会进行收集effectTag属性。

function ChildReconciler(shouldTrackSideEffects) {
  //...

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    //...
  }

  function reconcileChildrenIterator(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildrenIterable: Iterable<*>,
    lanes: Lanes,
  ): Fiber | null {
    //...
  }

  function reconcileSingleTextNode(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    textContent: string,
    lanes: Lanes,
  ): Fiber {
    //...
  }

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    //...
  } 

  function reconcileSinglePortal(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    portal: ReactPortal,
    lanes: Lanes,
  ): Fiber {
    //...
  } 

  // This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    //...
  } 

  return reconcileChildFibers;
}

ChildReconciler内部定义了许多协调相关的函数,并将reconcileChildFibers作为函数的返回值。也就是说ChildReconciler内部定义reconcileChildFibers才是React Diff真正的入口函数。reconcileChildFibers内部会根据newChild(即新生成的ReactElement)的类型和$$typeof属性调用不同的处理函数。

// 根据newChild进入不同Diff函数处理
function reconcileChildFibers(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
): Fiber | null {
  //...

  if (typeof newChild === 'object' && newChild !== null) {
    // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE:
        // 调用 reconcileSingleElement 处理
      // ...
    }
  }

  if (typeof newChild === 'string' || typeof newChild === 'number') {
    // 调用 reconcileSingleTextNode 处理
    // ...
  }

  if (isArray(newChild)) {
    // 调用 reconcileChildrenArray 处理
    // ...
  }

  if (getIteratorFn(newChild)) {
    // 调用 reconcileChildrenIterator处理
    // ... 
  }

  // ...

  // 如果以上分支都没有命中,则删除节点
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

我们可以根据newChild中节点的数量把Diff分为两种类型:

  1. 当newChild只有一个节点,也即newChild类型为object、number、string时,我们会进入单节点的Diff
  2. 当newChild有多个节点,也即类型为Array时,进入多节点的Diff

单节点的Diff

单节点指的是newChild为单一节点,但此节点在current fiber tree中对应的层级具有多少个节点我们是不确定的,因此我们可以根据此节点在current fiber tree中对应的层级的节点数量分为三种场景:

  1. 对应层级存在单个旧节点
旧: A
新: A'
  1. 对应层级存在多个旧节点
旧: A -> B -> C
新: A'
  1. 对应层级无旧节点
旧: null
新: A'

场景1和场景2我们可以归为一类(即在当前层级中,current fiber在sibling方向的链表中至少存在一个节点),场景3单独为一类(当前层级无current fiber节点)。

当前层级的current fiber在sibling方向的链表不为空

此时我们需要循环遍历当前层级在sibling方向的链表的所有current fiber节点,判断是否可以复用(key相同且type相同)当前层级的current fiber节点的stateNode(即复用fiber节点对应的DOM节点),可以则以current fiber作为副本生成workInProgress fiber。当前层级所有的current fiber节点都无法复用时,直接生成一个新的fiber节点作为workInProgress fiber

当前层级的current fiber在sibling方向的链表为空

由于不存在对应的current fiber节点,所以直接生成一个新的fiber节点作为workInProgress fiber

reconcileSingleElement的具体实现
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  //循环遍历当前这一层级的current fiber节点
  while (child !== null) {
    if (child.key === key) {
      //  key相同, 判断当前 current fiber.elementType 是否等于 ReactElement.type
      switch (child.tag) {
        //...
        default: {
          if (child.elementType === element.type) {
            //  type相同,表示可以复用当前fiber节点, 由于是单节点,所以如果当前节点存在兄弟节点, 需要给兄弟节点打上Deletion effectTag(删除兄弟节点)
            deleteRemainingChildren(returnFiber, child.sibling);
            // 构造workInProgress fiber节点,构造过程中会复用当前fiber节点的stateNode, 即复用DOM对象
            const existing = useFiber(child, element.props);
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            return existing;
          }
          break;
        }
      }
      // key相同但type不相同,则找到了当前节点在上次更新时对应的节点,但两个节点的type已经不一致,并且后续节点也没有可能进行复用,所以删除当前current fiber节点以及节点的剩余兄弟节点
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不一致则给当前current fiber节点打上Deletion effectTag,删除当前节点,并继续往下一个兄弟节点遍历,尝试匹配
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  //DOM节点不可复用 新建fiber作为ReactElement对应的WorkInProgress Fiber
  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.ref = coerceRef(returnFiber, currentFirstChild, element);
  created.return = returnFiber;
  return created;
}

注意,在为current fiber节点不匹配并打上Deletion effectTag时, 会同时将其添加到父节点的effectList中(正常effectList的收集是在completeWork中进行的, 但是被删除的节点会脱离fiber树, 无法进入completeWork的流程, 所以在beginWork阶段提前加入父节点的effectList);

多节点的Diff

newChild为多节点时,以下的几种场景中会触发多节点的Diff。

  1. 节点更新
//节点属性发生变更;
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

//新
<div key="a" className="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

//节点的类型发生变更;
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

//新
<span key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>
  1. 节点的数量发生改变;
//删除节点
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

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

//新增节点
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

//新
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>
<div key="d">d</div>
<div key="e">e</div>

  1. 节点的位置发生改变;
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>
<div key="e">e</div>

//新
<div key="a">a</div>
<div key="c">c</div>
<div key="b">b</div>
<div key="e">e</div>

我们在reconcileChildFibers的分析中可以看到,React使用两个函数reconcileChildrenArray(针对数组类型)和reconcileChildrenIterator(针对可迭代类型)来进行多节点的diff,我们重点看看reconcileChildrenArray

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
  //...
}

其中参数currentFirstChild指的是current fiber tree在该层级中的第一个fiber,通过 fiber.sibling我们可以得到一条从左到右串联currentFirstChild在当前层级的所有fiber节点的链表,我们使用oldFiber来表示这条链表。

参数newChildren是一个数组,包含了当前层级的所有ReactElement对象。

reconcileChildrenArray会对比这两个序列之间的差异,并生成workInProgress fiber tree在当前层级的fiber节点链表,并使用resultingFirstChild作为头节点。对比的过程会经历两轮的遍历。

第一轮遍历

  1. 遍历比较newChildrenoldFiber,判断DOM节点是否可复用。

  2. 如果可复用,则复用oldFiber生成newFiber并加入到以resultingFirstChild作为头节点的序列(后续我们使用newFiberList表示此序列)中,两个序列都跳到下一个节点。

3.如果不可复用,分两种情况进行处理:

  • 若key不一致,则立即终止第一轮遍历。
  • 若key相同但type不同,则删除当前oldFiber节点,生成新的fiber节点加入到newFiberList中,两个序列都跳到下一个节点,继续遍历。
  1. 当两个序列中的任意一个序列完成遍历(即newIdx >= newChildren.length - 1 || oldFiber.sibling === null),则结束第一轮遍历。

第二轮遍历

第一轮遍历后,会产生四种结果。

oldFibernewChildren都完成遍历了

此时本轮diff已经完成了。这种情况是最理想的情况,只需要进行第一轮遍历中对应的更新操作。

oldFiber遍历完了,但newChildren还没遍历完。

假设newFiberList中所有节点都可以是可以直接复用的,那么当前场景和我们之前讨论过的新增节点的场景是一致的。

//新增节点
//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

//新
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>
<div key="d">d</div>
<div key="e">e</div>

这意味着未遍历完的newChildren节点都是新增的,我们需要遍历剩下的newChildren,生成对应的workInProgress fiber并标记上Placement。

此处只是假设第一轮遍历中所有加入到newFiberList的节点都是可以复用的,便于大家理解,但前面我们有分析过,在第一轮遍历中能够加入到newFiberList的节点有两种:

  1. 可以直接复用的节点;
  2. key相同但type不同的节点;
newChildren遍历完了,但oldFiber还没遍历完

我们再次假设加入到newFiberList中所有节点都可以是可以直接复用的,这次就和我们之前讨论过的删除节点的场景是一致的了。

//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>

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

此时newChildren节点都已经找到了可复用的节点,我们需要遍历剩下的oldFiber,并标记上Deletion。

newChildren与oldFiber都没完成遍历

这意味着这次更新中有可能存在节点改变了位置。我们以前面提到的位置移动的例子来进行分析。

//旧
<div key="a">a</div>
<div key="b">b</div>
<div key="c">c</div>
<div key="e">e</div>

//新
<div key="a">a</div>
<div key="c">c</div>
<div key="b">b</div>
<div key="e">e</div>

经过第一轮遍历后,key值为a的节点复用oldFiber节点生成newFiber节点,并加入到了newFiberList中。在进行第二个节点的比较时,新节点c和旧节点b的key值不一致,此时我们会终止第一轮遍历,进入第二轮遍历。

由于可能存在移动的节点,所以节点的索引值不能够找出两个对应的节点,我们需要一个找到新旧节点对的方法。这时,节点的key值就起作用了。我们前面提到:

开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变;

React会将还未处理的oldFiber节点存入以节点key为key,oldFiber为value的Map中。

//existingChildren 是一个以节点key为key,oldFiber为value的Map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

然后继续遍历newChildren序列,并通过newChildren节点的key来查询existingChildren中是否存在key相同的oldFiber节点。

接下来我们需要通过第二轮遍历去标记哪些节点可以复用,哪些节点需要移动。此时我们需要一个参照索引来判断节点是否需要移动。

我们以newFiberList中最靠右的节点对应的oldFiberoldFiber链表中的位置索引oldIndex作为参照索引,并使用lastPlacedIndex表示参照索引。

上面分析的例子中,在结束第一轮遍历后,lastPlacedIndex就是newFiberList的最右一个节点a的oldIndex,也就是0。

然后开始第二轮的遍历

newChildren节点c
newChildren节点c通过existingChildren查询到了oldFiber节点c,发现oldFiber节点c的索引oldIndex2 ,大于lastPlacedIndex=0

lastPlacedIndex的定义可知,若当前的oldIndex大于lastPlacedIndex,意味着oldFiber节点c的位置是在newFiberList的最右一个节点对应的oldFiber节点的右边。同时,由于newChildren序列是按顺序遍历的,所以当前的newChildren生成的newFiber节点一定是在newFiberList的最右一个节点对应的newFiber节点的右边。因此,节点c更新前后的位置没有发生。

此时,因为节点c的oldIndex > lastPlacedIndex,所以我们把lastPlacedIndex的值更新为节点c的oldIndex=2。同时把节点c生成的newFiber加入到newFiberList中,继续遍历。

newChildren节点b
newChildren节点c通过existingChildren查询到了oldFiber节点c,发现oldFiber节点c的索引oldIndex1,小于lastPlacedIndex=2oldFiber节点b的位置是在newFiberList的最右一个节点对应的oldFiber节点的左边。但newChildren节点b生成的newFiber节点一定是在newFiberList的最右一个节点对应的newFiber节点的右边的。因此,节点b更新前后的位置会发生变化。

此时,因为节点b的oldIndex < lastPlacedIndex,所以lastPlacedIndex不变,并把节点b生成的newFiber标记Placement,表示该节点更新前后的位置会发生变化,最后把newFiber加入到newFiberList中,继续遍历。

newChildren节点d
newChildren节点d通过existingChildren查询到了oldFiber节点d,发现oldFiber节点d的索引oldIndex3 ,大于lastPlacedIndex=3。 处理逻辑与节点c一致,不再展开。

diff完成,我们也得到了该层级中完整的newFiberList,作为workInProgress fiber tree在该层级的fiber节点。

我再来看看另一个例子

//旧
A->B->C->D

//新
A->D->B->C
image.png

我们可以看到,节点D时不会移动的,只有当节点需要向左移动时(本例子中就是节点B、节点C),节点的位置才会发生变更,因此我们要尽量减少将节点从后面移动到前面的操作。

reconcileChildrenArray的具体实现

最后我们来看看reconcileChildrenArray是如何实现上面的逻辑的。

  /* * returnFiber:当前层级的节点的父节点,即currentFirstChild的父级fiber节点
     * currentFirstChild:当前层级的第一个current fiber节点
     * newChildren:当前层级的ReactElement节点
     * lanes:优先级相关
  * */
  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    //..

    // workInProgress fiber tree在此层级的第一个fiber节点,即该层级上在fiber.sibling方向的链表的头节点,在上面的分析中我们使用newFiberList来表示此序列
    let resultingFirstChild: Fiber | null = null;
    // previousNewFiber用来将后续的新生成的workInProgress fiber连接到newFiberList中
    let previousNewFiber: Fiber | null = null;

    // oldFiber节点,用于访问workInProgress fiber tree在此层级上在fiber.sibling方向的链表
    let oldFiber = currentFirstChild;
    // 用于存储newFiberList中最右边的节点在oldFiber链表中的index
    let lastPlacedIndex = 0;
    // 存储遍历到newChildren的索引
    let newIdx = 0;
    // 存储当前oldFiber的下一个节点
    let nextOldFiber = null;

    // 第一轮遍历
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }

       // 尝试生成新的节点,如果key不同, 返回null,key相同, 再比较type是否一致。type一致则执行useFiber(update逻辑),type不一致则运行createXXX(insert逻辑)
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
      // newFiber为 null说明节点不可复用,中断当前遍历
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
      // shouldTrackSideEffects 为true 代表当前为update阶段
        if (oldFiber && newFiber.alternate === null) {
          // 此时为key相同但type不同,newFiber.alternate为空,删除掉oldFiber
          deleteChild(returnFiber, oldFiber);
        }
      }
      
      //更新记录lastPlacedIndex
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      //将newFiber添加到newFiberList中
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // newChildren遍历完了,删除oldFiber链中未遍历的节点
      deleteRemainingChildren(returnFiber, oldFiber);
     
      //...
      //结束此次diff
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // oldFiber遍历完了,新增newChildren未遍历的节点到newFiberList中
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      //...
      //结束本次diff
      return resultingFirstChild;
    }

    // newChildren和oldFiber都没有遍历完,将oldFiber剩余序列加入到一个map中,在第二次遍历的过程中能够通过key值找到对应的oldFiber
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // 第二轮遍历
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // 如果newFiber是通过复用创建的, 则清理map中对应的老节点
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        // 判断当前节点是否需要改变位置,并更新记录lastPlacedIndex
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        // 添加newFiber到newFiberList中
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      //删除未遍历的oldFiber
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    
    //...

    //结束本次diff
    return resultingFirstChild;
  }

  //此函数用于记录节点在此次更新中是否需要移动,并返回新的lastPlacedIndex 
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    
    //...

    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        // 此节点需要移动
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        // 此节点不需要移动,返回oldIndex作为lastPlacedIndex的值
        return oldIndex;
      }
    } else {
      // 这是插入的逻辑
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
  }

探索React源码系列文章

探索React源码:初探React fiber

探索React源码:React Diff

探索React源码:Reconciler

最后编辑于:2024-10-27 15:39:02


喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/907552.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

开源库 FloatingActionButton

开源库FloatingActionButton Github:https://github.com/Clans/FloatingActionButton 这个库是在前面这个库android-floating-action-button的基础上修改的&#xff0c;增加了一些更强大和实用的特性。 特性&#xff1a; Android 5.0 以上点击会有水波纹效果 可以选择自定义…

微服务设计模式 - 重试模式(Retry Pattern)

微服务设计模式 - 重试模式&#xff08;Retry Pattern&#xff09; 定义 重试模式&#xff08;Retry Pattern&#xff09;是一种微服务中的设计模式&#xff0c;用于在临时性失败&#xff08;如网络故障或暂时不可用的服务&#xff09;发生时&#xff0c;自动重新尝试请求&…

HTML 基础标签——链接标签 <a> 和 <iframe>

文章目录 1. `<a>` 标签属性详细说明示例2. `<iframe>` 标签属性详细说明示例注意事项总结链接标签在HTML中是实现网页导航的重要工具,允许用户从一个页面跳转到另一个页面或嵌入外部内容。主要的链接标签包括 <a> 标签和<iframe> 标签。本文将深入探…

Netty 组件介绍 - Future Promise

在异步处理时&#xff0c;经常用到这两个接口 netty 中的 Future 继承 jdk 中的 FutuFuture&#xff0c;而Promise 又对 netty Future 进行了扩展。 idk Future 只能同步等待任务结束&#xff08;或成功或失败)才能得到结果netty Future 可以同步等待任务结束得到结也可以异…

Excel:vba实现批量插入图片批注

实现的效果&#xff1a;实现的代码如下&#xff1a; Sub InsertImageNamesAndPictures()Dim PicPath As StringDim PicName As StringDim PicFullPath As StringDim RowNum As IntegerDim Name As StringDim Comment As CommentDim folder As FileDialog 定义文件选择对话框 清…

C++(类和对象-友元)

友元的作用 作用&#xff1a; 在C中&#xff0c;友元&#xff08;friend&#xff09;是一种特殊的类成员&#xff0c;它可以让一个函数或者类访问其他类的私有&#xff08;private&#xff09;和保护&#xff08;protected&#xff09;成员。 注意&#xff1a; 友元的使用应该谨…

ssm044基于java和mysql的多角色学生管理系统+jsp(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 题目&#xff1a;学生管理系统设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本学生管理系统就是在这…

猜字谜 华为OD

源码 Java import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test;import java.util.ArrayList; import java.util.List; import java.util.TreeSet;public class GuessWord {public Input input;BeforeEachpublic void init() {input new Input("…

C++队列

好久没有发博客了&#xff0c;欢迎阅读&#xff1a;C队列。 1.队列的介绍 队列&#xff08;queue&#xff09;是一种先进先出的、操作受限的线性表。 数据结构是&#xff1a;先进入队列的先出去&#xff0c;后进入队列的后出去。 必须从队尾插入新元素&#xff0c;队列中的…

【ArcGISPro】制作简单的ArcGISPro-AI助手

【python】AI Navigator的使用及搭建本机大模型_anaconda ai navigator-CSDN博客 【Python】AI Navigator对话流式输出_ai大模型流式输出效果(打字效果) python-CSDN博客 【C#】调用本机AI大模型流式返回_怎么实现调用本地大模型时实现流式输出-CSDN博客 【ArcGISPro】宣布推…

小白从零开始配置pytorch环境

一、下载ANACONDA 官方网址Anaconda Installers and Packages 笔者选择的是Anaconda3-5.3.0-Windows-x86_64.exe版本。全程安装可以手机开热点&#xff0c;会快一点。 二、查看电脑是否有显卡 1、打开任务管理器 2、查看电脑CUBA版本&#xff0c;如上篇文章所提到查看CUDA-V…

Java设计模式之责任链模式

1、责任链模式的定义&#xff1a; 责任链模式(Iterator Pattern)是一种行为型设计模式&#xff0c;使多个对象都有机会处理同一个请求&#xff0c;将这些对象连成一条链&#xff0c;并沿着这条链传递该请求&#xff0c;直到有一个对象处理它为止。 2、责任链模式的角色&#x…

web安全测试渗透案例知识点总结(下)——小白入狱

目录 [TOC](目录)一、更多详细的实际案例教程案例1&#xff1a;文件上传漏洞利用案例2&#xff1a;目录遍历&#xff08;Path Traversal&#xff09;漏洞检测案例3&#xff1a;暴力破解登录密码案例4&#xff1a;命令注入漏洞案例5&#xff1a;身份认证绕过&#xff08;Passwor…

.NET 8 中 Entity Framework Core 的使用

本文代码&#xff1a;https://download.csdn.net/download/hefeng_aspnet/89935738 概述 Entity Framework Core (EF Core) 已成为 .NET 开发中数据访问的基石工具&#xff0c;为开发人员提供了强大而多功能的解决方案。随着 .NET 8 和 C# 10 中引入的改进&#xff0c;开发人…

后端java——如何为你的网页设置一个验证码

本文通过HUTOOL实现&#xff1a;Hutool参考文档Hutool&#xff0c;Java工具集https://hutool.cn/docs/#/ 1、工具的准备 如果我们通过hutool来实现这个功能&#xff0c;我们需要提前安装hutool的jar包。 下载地址&#xff1a;Central Repository: cn/hutool/hutool-all/5.8.…

Java面试经典 150 题.P189. 轮转数组(006)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解&#xff1a; class Solution {public void rotate(int[] nums, int k) {…

Cesium基础-(Entity)-(ellipsoid)

里边包含Vue、React框架代码详细步骤、以及代码详细解释 7、ellipsoid 球体与椭球体 Ellipsoid(椭球体)是 Cesium 中用来表示地球或其他天体形状的几何对象。在三维空间中,椭球体是一个被拉伸或压缩的球体,它由三个半径定义:沿着 x、y 和 z 轴的半径。这些半径确定了椭球体…

nvm详解

本文借鉴转载于 nvm文档手册 文章目录 1.nvm是什么&#xff1f;2.nvm安装2.1 window上安装下载链接安装步骤 2.2 Mac上安装使用homebrew 安装 nvm 3.nvm使用指令 1.nvm是什么&#xff1f; nvm&#xff08;Node Version Manager&#xff09;是一个用于管理和切换不同版本 Node.…

【辽宁】《辽宁省省级政务信息化建设项目预算支出标准规定(试行)》(辽财预〔2021〕54号)-省市费用标准解读系列04

《辽宁省省级政务信息化建设项目预算支出标准规定&#xff08;试行&#xff09;》&#xff08;辽财预〔2021〕54号&#xff09;是由辽宁省财政厅和辽宁省信息中心于2021年发布应用的信息化建设项目预算支出标准。我司基于专业第三方信息化项目造价机构角度&#xff0c;从标准创…

基于vue3和elementPlus的el-tree组件,实现树结构穿梭框,支持数据回显和懒加载

一、功能 功能描述 数据双向穿梭&#xff1a;支持从左侧向右侧转移数据&#xff0c;以及从右侧向左侧转移数据。懒加载支持&#xff1a;支持懒加载数据&#xff0c;适用于大数据量的情况。多种展示形式&#xff1a;右侧列表支持以树形结构或列表形式展示。全选与反选&#xf…