React之Diff 算法

news2024/10/11 4:31:00

在 React 中,通过 React.createElement 也能生成一个虚拟 DOM 节点(ReactElement)。在 React15 及以前,采用了递归的方式创建虚拟 DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。React16 将递归的无法中断的更新重构为异步的可中断更新,推出了新的 Fiber 架构。

原本的 ReactElement 只有 children,在中断恢复时,无法找到其兄弟节点和父节点,无法从断点处继续完成渲染工作。而 fiber 节点上能访问到父节点、子节点、兄弟节点,所以即使渲染被打断了,也可以恢复查找未处理的节点。因此,React 需要先生成 ReactElement,再生成 fiber,最后才将变更映射到真实 DOM 节点。Vue 与 React 不同,它通过递归的形式生成整个虚拟 DOM 树,在 diff 的同时会对 DOM 做变更。

React 采用了双缓存的技术,在 React 中最多会存在两颗 fiber 树,当前屏幕上显示内容对应的 fiber 树称为 current fiber 树,正在内存中构建的 fiber 树称为 workInProgress fiber 树。当 workInProgress fiber 树构建并渲染到页面上后,应用根节点的 current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树。

React 的更新会经历两个阶段:render 阶段 和 commit 阶段。render 阶段是可中断的,commit 阶段是不可中断的。

  • render 阶段会生成 fiber 树,所谓的 diff 就会发生在这个阶段。React 通过深度优先遍历来生成 fiber 树,整个过程与递归是类似的,因此生成 fiber 树的过程又可以分为「递」阶段和「归」阶段。

  • commit 阶段主要执行各种 DOM 操作、生命周期钩子、某些 hook 等。

因此,diff 阶段不会直接变更 DOM,而是留到 commit 阶段再做变更。

React 如何知道哪些 DOM 节点需要被更新呢?

render阶段的beginWork函数中,会将上次更新产生的 Fiber 节点与本次更新的 JSX 对象(对应ClassComponentthis.render方法返回值,或者FunctionComponent执行的返回值)进行比较。根据比较的结果生成workInProgress Fiber,即本次更新的 Fiber 节点。即,React 将上次更新的结果与本次更新的值比较,只将变化的部分体现在 DOM 上。这个比较的过程,就是 Diff。

由于 Diff 操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n^3 ),其中 n 是树中元素的数量。

O(n³) 由来

关于 O(n³) 的由来。由于左树中任意节点都可能出现在右树,所以必须在对左树深度遍历的同时,对右树进行深度遍历,找到每个节点的对应关系,这里的时间复杂度是 O(n²),之后需要对树的各节点进行增删移的操作,这个过程简单可以理解为加了一层遍历循环,因此再乘一个 n。

为了降低算法复杂度,React 的 diff 会预设三个限制:

  • Tree Diff(树策略): 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么 React 不会尝试复用他。 因为,Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  • Component Diff(组件策略): 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • Element Diff(元素策略):对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。开发者可以通过 key 属性来暗示哪些子元素在不同的渲染下能保持稳定。

React 进行 tree diffcomponent diff 和 element diff进行算法优化是基于上面三个前提策略。事实证明上面的三个前提策略是非常有效的。

Diff 入口

function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // ...

    // 判断 newChild 类型
    const isObject = typeof newChild === 'object' && newChild !== null;

    // object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE 等  
    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          // 调用 reconcileSingleElement 处理
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          // 调用 reconcileSinglePortal 处理    
          // ....
        case REACT_LAZY_TYPE:
          // 调用 reconcileChildFibers 处理    
          // ....
      }
    }

    // newChild 是字符串或数字 调用 reconcileSingleTextNode 处理   
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }

    // newChild 是数组 调用 reconcileChildrenArray 处理
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
 // newChild 迭代器 reconcileChildrenIterator 处理
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    // ....    

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

可以从同级的节点数量将 Diff 分为两类:

  • 单节点条件:当newChild类型为objectnumberstring,代表同级只有一个节点

  • 多节点条件:当newChild类型为Array,同级有多个节点

单节点 Diff

对于单个节点,当newChild类型为objectnumberstring,代表同级只有一个节点,会进入reconcileSingleElement

function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
        
    // 首先判断是否存在对应 DOM 节点
    while (child !== null) {
      // 上一次更新存在 DOM 节点,接下来判断是否可复用
      // 首先比较 key 是否相同  
      if (child.key === key) {
        // key 相同,接下来比较 type 是否相同
        switch (child.tag) {
          case Fragment: // ...
          case Block: // ...
          default: {
            if (child.elementType === element.type ) {
              // 情况一:key 和 type 都相同则表示可以复用 返回复用的 fiber
              return existing;
            }
            // type 不同则跳出 switch
            break;
          }
        }
        // Didn't match.
        // 情况二:key 相同但是 type 不同 将该 fiber 及其兄弟 fiber 标记为删除
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // 情况三:key 不同,Type也不同,将该 fiber 标记为删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
 // 创建新 fiber
    if (element.type === REACT_FRAGMENT_TYPE) {
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

React 通过先判断 key 是否相同,如果 key 相同则判断 type 是否相同,只有都相同时一个 DOM节点 才能复用。

  • child !== nullkey相同type不同时执行deleteRemainingChildrenchild及其兄弟fiber都标记删除。
  • child !== nullkey不同时仅将child标记删除。
小结
  • 单节点条件:newChild 类型为 objectnumberstring,代表同级只有一个节点。
  • 单节点复用条件:key 和 type 都相等,否则不可复用节点。
    • key 相同:child.key === key。没有设置 key 则为 null。新旧节点 key 都为 null,也认为 key 相同。
    • type 相同:child.elementType === element.type
  • 单节点 diff 规则:child 存在的情况下(有节点),先判断 key 是否相同,如果 key 相同则判断 type 是否相同,只有都相同时一个 DOM节点 才能复用。
    • key 相同 & type 相同:可以复用 返回复用的 fiber
    • key 相同 & type 不同:将该 fiber 及其兄弟 fiber 标记为删除
    • key 不同:将该 fiber 标记为删除
  • 没有节点,则直接新建

多节点 Diff

React 每次更新时,会将新的 ReactElement(即 React.createElement() 的返回值)与旧的 fiber 树作对比,比较出它们的差异后,构建出新的 fiber 树,因此多节点的 diff 实际上是用 fiber(旧子节点)和 ReactElement 数组(新子节点)进行对比。多节点 DIff 无非以下情况:

  • 节点更新(属性、类型)
  • 节点新增或删除
  • 节点位置变化

在日常开发中,相较于新增删除更新组件发生的频率更高。所以Diff会优先判断当前节点是否属于更新

多节点 Diff算法的整体逻辑会经历两轮遍历:

  • 第一轮遍历:处理更新的节点。
  • 第二轮遍历:处理剩下的不属于更新的节点。
function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>, // 新节点
    lanes: Lanes,
  ): Fiber | null {
    // 列表中使用 React element 数据更新了属性的第一个 Fiber 节点
    let resultingFirstChild: Fiber | null = null;
    // 上一个更新了属性的 Fiber 节点,用以列表中兄弟节点的相互关联
    let previousNewFiber: Fiber | null = null; 
    // current 树上的列表中的第一个Fiber节点
    let oldFiber = currentFirstChild; 
    // 上一个元素移动位置的下标    
    let lastPlacedIndex = 0; 
    // 遍历 React element 树的下标    
    let newIdx = 0; 
    // current 树上的列表中元素的兄弟节点    
    let nextOldFiber = null; 
        
    // 第一轮遍历:这个 for 循环的作用是剔除没有变化的节点,并对节点进行更新和重用
    // 当检查到尾部有新增节点时,oldFiber 为 null,则会跳出循环,
    // 然后创建新的 Fiber 插入到尾部,不需要与其它节点进行对比
    // 当有新节点想在中间插入,newFiber 则为 null,则会跳出循环,然后需要移动节点位置进行排序
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 判断旧 Fiber 节点上的 key 与 React element 上的 key 属性是否相等
      // key 相等,则会使用 React element 的数据更新 Fiber 节点上的属性
      // key 不相等,则会返回 null
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx], // 遍历 newChildren
        lanes,
      );
      // 发现有元素的 key 属性有变化,说明不是更新场景,则会跳出 for 循环
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      // 将 newIdx 赋值给 workInProgress 树上的 Fiber 节点的 index 属性,代表当前元素在列表中的位置(下标)
      // 判断 current 树上元素的 Index 是否小于 lastPlacedIndex,是则表示该元素需要移动位置,否则表示不需要移动位置
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        // 将更新了属性的兄弟 Fiber 节点进行关联
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    // 新的子节点已经遍历完成,如果还有剩下的节点,表示 current 树上有,
    // 但是 workInProgress 树上没有的节点,需要全部删除
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    // 节点新增,不需要与旧节点对比,直接创建新增
    if (oldFiber === null) {
      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;
      }
      return resultingFirstChild;
    }

    // 将 current 树上的列表中还未对比的元素添加进 Map 对象中
    // 下面的 for 循环会根据 key 取出 Map 中对应的旧的 Fiber 与 React element 做类型的比较
    // 如果类型相同则更新 Fiber 属性,不同,则会根据 React element 重新创建一个新的 Fiber 做插入操作
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // 节点移动
    for (; newIdx < newChildren.length; newIdx++) {
      // 根据 key 取出 Map 中对应的旧的 Fiber 与 React element 做类型的比较
      // 如果类型相同则使用 React element 的数据更新 Fiber 节点上的属性进行重用
      // 不同,则会根据 React element 的数据重新创建一个新的 Fiber 做插入操作 
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        
        if (shouldTrackSideEffects) {
          // newFiber.alternate 不为 null,表示是重用的节点,需要将 existingChildren 中重用的节点删除掉
          // 遍历结束后 existingChildren 中剩下的节点,则是需要删除的    
          if (newFiber.alternate !== null) {
            // 在调用 updateFromMap 方法时,会根据 key 取出相对应的 Fiber
            // 调用 updateFromMap 方法完成后,对应 key 的 Fiber 值被重用了,所以需要删除 Map 中使用过的 key 对应的值  
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        // 将 newIdx 赋值给 workInProgress 树上的 Fiber 节点的 index 属性,代表当前元素在列表中的位置(下标)
        // 判断 current 树上元素的 Index 是否小于 lastPlacedIndex,是则表示该元素需要移动位置,否则表示不需要移动位置。  
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }
    // 节点删除
    if (shouldTrackSideEffects) {
      // existingChildren 中剩下的 Fiber,表示 current 树上存在,但是 workInProgress 树上不存在的元素
      // 将剩下的 Fiber 添加到父 Fiber 节点的 deletions 属性中, 
      // 并且在 flags 集合中添加删除标识,在 commit 阶段会将这些元素进行删除
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }
    // 返回列表中的第一个节点
    return resultingFirstChild;
  }

 

第一轮遍历

从前到后遍历新旧子节点

  • key 和 type 都相同,则根据旧 fiber 和新 ReactElement 的 props 生成新子节点 fiber。即, 使用 React element 的数据更新 Fiber 节点上的属性。 复用旧节点,只更新其属性 props(当然也包含 children)
  • key 相同,但 type 不同,将根据新 ReactElement 生成新 fiber,旧 fiber 将被添加到它的父级 fiber 的 deletions 数组中,后续将被移除。创建新节点,删除旧节点。
  • key 不同,结束遍历。

 

第二轮遍历

如果第一轮遍历被提前终止了,意味着还有新 ReactElement 或 旧 fiber 还未被遍历。因此会有第二轮遍历去处理以下三种情况:

  • 只剩旧子节点

    • 说明多余的 oldFiber 在这次更新中已经不存在了,所以需要遍历剩下的 oldFiber,依次执行删除操作(Fiber.effectTag = Deletion
  • 只剩新子节点

    • 说明老的 DOM 节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的 newChildren 依次执行插入操作(Fiber.effectTag = Placement
  • 新旧子节点都有剩

    • 说明有节点在这次更新中改变了位置,需要移动节点。由于有节点交换了位置,所以不能再用位置索引对比前后的节点,那么怎样才能将同一个节点在两次更新中对应上呢?这时候就需要用 key 属性了。为了快速的找到 key 对应的 oldFiber,将所有还没处理的 oldFiber 放进以 key 属性为 key,以 Fiber 为 value 的 map,空间换时间。

 

只剩旧子节点

只剩下旧子节点的处理方法很简单,只需要将剩余的 旧 fiber 放到父 fiber 的 deletions 数组中,这些旧 fiber 对应的 DOM 节点将会在 commit 阶段被移除

 

只剩新子节点

对于剩余的新子节点,先创建新的 fiber 节点,然后打上 Placement 标记,我们将在遍历 fiber 树的「归」阶段生成这些新 fiber 对应的 DOM 节点。

 

新旧子节点都有剩

由于有节点交换了位置,所以不能再用位置索引对比前后的节点,那么怎样才能将同一个节点在两次更新中对应上呢?这时候就需要用 key 属性了。为了快速的找到 key 对应的 oldFiber,将所有还没处理的 oldFiber 放进以 key 属性为 key,以 Fiber 为 value 的 map,空间换时间。

这种情况下,需要一个快速的方法帮助我们快速找到某个 ReactElement 在上一次渲染时生成的 fiber 节点。因此,我们需要一个 existingChildren Map,这个 Map 保存了旧 fiber 的 key 到 旧 fiber 的映射关系,我们可以通过新的 ReactElement 的 key 快速在这个 Map 中找到对应的旧 fiber

  • 能找到,则能复用旧 fiber 以生成新 fiber
  • 找不到,证明要生成新的 fiber,并打上一个 Placement 标志,以便于在 commit 阶段插入该 fiber 对应的 DOM 节点。

我们还需要找到哪些节点的位置发生了变化。

假设我们有 a、b、c、d 四个节点,它们的位置索引是 0、1、2、3,是一个递增的序列。在更新后,它们顺序发生了变化,变成了 a、c、b、d,那么它们的位置索引变为 0、2、1、3(继续沿用旧的位置索引),不再是一个递增的序列,因为索引 1 移到了 2 的后面(即 b 原本在 c 前面,更新后被移到了 c 后面),破坏了递增的规律。因此,只需要找到那些破坏了索引递增规律的节点,就知道哪些节点的位置发生了变化。那具体要怎么做呢?

其实,旧 fiber 上有 index 属性,index 属性记录了在上一次渲染时该 fiber 所在的位置索引。

  • oldIndex:当前可复用节点在旧节点上的位置索引
  • lastPlacedIndex:把遍历新子节点过程中访问过的最大 oldIndex。该变量表示当前最后一个可复用节点,对应的 oldFiber 在上一次更新中所在的位置索引。我们通过这个变量判断节点是否需要移动。

如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动,并将 lastPlacedIndex = oldIndex; 如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动。那么,只要当前新子节点有对应的旧 fiber,且 oldIndex < lastPlacedIndex,就可以认为该新子节点对应的 DOM 节点需要往后移动,并打上一个 Placement 标志,以便于在 commit 阶段识别出这个需要移动 DOM 节点的 fiber。

遍历流程
  • 遍历未处理的旧子节点,生成 existingChildren Map

  • 从前到后遍历新子节点

    • 如果能在 existingChildren Map 中找到对应的旧 fiber,根据旧 fiber 生成新 fiber;如果不能,生成新 fiber,并打上 Placement 标志
    • 从 existingChildren Map 中删除已处理的节点
    • 如果新子节点有对应的旧 fiber
      • 当 oldIndex < lastPlacedIndex 时,给新 fiber 打上 Placement 标志;否则,令 lastPlacedIndex = newIndex
    • 如果新子节点没有对应的旧 fiber,创建一个新 fiber 并 打上 Placement 标志
  • 遍历 existingChildren Map,将 Map 中所有节点添加到父节点的 deletions 数组中

 

Commit 阶段变更

DOM 元素类型的 fiber 节点上存有对 DOM 节点的引用,因此在 commit 阶段,深度优先遍历每个新 fiber 节点,对 fiber 节点对应的 DOM 节点做以下变更:

  • 删除 deletions 数组中 fiber 对应的 DOM 节点
  • 如有 Placement 标志,将节点移动到往后第一个没有 Placement 标记的 fiber  DOM 节点之前
  • 更新节点。以 DOM 节点为例,在生成 fiber 树的「归」阶段,会找出属性的变更集,在 commit 阶段更新属性。

 

性能缺陷

React 采用仅右移方案,在大部分从左往右移的业务场景中,得到了较好的性能。但在处理节点左移(前移),表现就不太乐观。

以下图为例,节点 a、b、c、d 变为了d、a、b、c,如果我们手动处理这种位置变化,只需要一步:将 d 节点移动到 a 前面。但 React 实际上的做法有三步:将 a、b、c 三个节点依次插入到 d 节点后面。

 

这是因为遍历完 d 节点后,lastPlacedIndex 变成了 3,再去遍历 a、b、c、d 时,**oldIndex 一定小于 lastPlacedIndex**了。

因此,实际编写代码时,应该尽量避免节点往前移动的操作。

为什么不用双端 Diff

既然 React 对节点往前移动的情况处理得不好,是不是可以在每次遍历的时候,都尝试和旧子节点中最后一个未处理节点做对比,看看能不能匹配上。实际上,Vue2 的 diff 就是这么做的。

Vue 2 的双端 diff 是旧的一组 VNode(旧子节点)和新的一组 VNode(新子节点)进行对比

所谓的「双端」,表示在新旧子节点的数组中,各用两个指针指向头尾节点,在遍历过程中,头尾指针不断靠拢。因此,用 newStartIndex 和 newEndIndex 分别指向新子节点中未处理节点的头尾节点,用 oldStartIndex 和 oldEndIndex 分别指向旧子节点中未处理节点的头尾节点。

现在,我们用「新前」表示新子节点中未处理节点的第一个节点;用「新后」表示新子节点中未处理节点的最后一个节点;「旧前」表示旧子节点中未处理节点的第一个节点;用「旧后」表示旧子节点中未处理节点的最后一个节点。

每遍历到一个节点,就尝试进行双端比较:「新前 vs 旧前」、「新后 vs 旧后」、「新后 vs 旧前」、「新前 vs 旧后」,如果匹配成功,更新双端的指针。比如,新旧子节点通过「新前 vs 旧后」匹配成功,那么 newStartIndex += 1,oldEndIndex -= 1。

如果新旧子节点通过「新后 vs 旧前」匹配成功,还需要将「旧前」对应的 DOM 节点插入到「旧后」对应的 DOM 节点之前。如果新旧子节点通过「新前 vs 旧后」匹配成功,还需要将「旧后」对应的 DOM 节点插入到「旧前」对应的 DOM 节点之前。

如果通过双端比较都没法找到匹配的节点,就需要一个像 React existingChildren Map 的 Map 对象了,在 Vue2 的 diff 中,这个 Map 名字叫做 oldKeyToIdx Map。通过这个 Map,遍历时就可以尝试根据新子节点的 key 去找 oldIndex,查找结果会有两种:

  • 找到 oldIndex,即新旧子节点中有相同 key 的节点。

    • 如果 VNode 的 type 是相同的,将旧子节点对应的 DOM 节点插入到「旧前」对应的 DOM 节点之前。
    • 如果 VNode 的 type 是不同的,创建一个新的 DOM 节点,并插入到「旧前」对应的 DOM 节点之前。
  • 没找 oldIndex,需要根据新子节点(VNode)创建 DOM 元素,并插入到「旧前」对应的 DOM 节点之前。

简单来说,第一轮遍历会先尝试比较新旧子节点的双端节点,如果匹配不成功,再尝试在旧子节中找到对应的节点。至于 DOM 节点的移动,需要记住只能移动到「旧前」之前或「旧后」之后。如果更新后节点位置被调到前面了,移动时就需要移到「旧前」之前;如果更新后节点位置被调到后面了,移动时就需要移到「旧后」之后。

  • 如果第一轮遍历后,只剩下新子节点(oldStartIndex > oldEndIndex),则根据剩余的新子节点(VNode)创建 DOM 节点,并依次插入到父级 DOM 节点最后。

  • 如果第一轮遍历后,只剩下旧子节点(newStartIndex > newEndIndex),则将剩余旧子节点对应的 DOM 节点依次从父级 DOM 节点中删除。

需要注意的是,Vue 在 diff 的过程中,会直接进行节点的更新/新建/删除操作,这点和 React 是不同的。

React 在源码注释中解释了为什么不使用双端 diff

由于双端 diff 需要向前查找节点,但每个 fiber 节点上都没有反向指针,即前一个 fiber 通过 sibling 属性指向后一个 fiber,只能从前往后遍历,而不能反过来(你可以在上文的各个示例图中看到这种实现),因此该算法无法通过双端搜索来进行优化。React 想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。React 认为对于列表反转和需要进行双端搜索的场景是少见的。单链表无法使用双指针,所以无法对算法使用双指针优化。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

达梦配置ODBC连接

达梦配置ODBC连接 基础环境 操作系统&#xff1a;Red Hat Enterprise Linux Server release 7.9 (Maipo) 数据库版本&#xff1a;DM Database Server 64 V8 架构&#xff1a;单实例1 下载ODBC包 下载网址&#xff1a;https://www.unixodbc.org/ unixODBC-2.3.0.tar.gz2 编译并…

树状数组-数据结构

树状数组 t[x] 节点的父节点为 t[x lowbit(x)] 整棵树的深度为 log2n 1 1 . add(x,k) 给指定的节点x加上k — 动态的维护前缀和 需要从x开始&#xff0c;向上找到所有父节点&#xff0c;值都加上k 2. ask(x) 求取节点x之前的前缀和 求取单点之前的前缀和只需要累加即可 …

redis群集有三种模式

目录 redis群集有三种模式 redis群集有三种模式 分别是主从同步/复制、哨兵模式、Cluster ●主从复制&#xff1a;主从复制是高可用Redis的基础&#xff0c;哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份&#xff0c;以及对于读操作的负载均…

LeetCode | 数组 | 二分查找 | 35.搜索插入位置【C++】

题目链接 题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出…

数据结构——图的应用(最小生成树,最短路径,拓扑排序,关键路径)

目录 1.最小生成树 1.概念回顾——生成树 2.最小生成树概念 2.构造最小生成树 1.MST性质 2.Prim算法 3.Kruskal 算法 4.两种算法比较 3.最短路径 1.两点间最短路径 2.某源点到其它各点最短路径 3.单源最短路径——用Dijkstra算法 4.所有顶点间的最短路径…

Echarts 自适应宽高,或指定宽高进行自适应

文章目录 需求分析 需求 有一个按钮实现对Echarts的指定缩放与拉长&#xff0c;形成自适应效果 拉长后效果图 该块元素缩短后效果图 分析 因为我习惯使用 ref 来获取组件的 DOM 元素&#xff0c;然后进行挂载 <div ref"echartsRef" id"myDiv" :sty…

Shell脚本之基本语法

目录 一、变量定义 变量命名规则&#xff1a; 变量的赋值&#xff1a; 只读变量&#xff1a; 删除变量&#xff1a; 二、变量的类型 自定义变量&#xff1a; 环境变量&#xff1a; 位置参数&#xff1a; 预定义变量&#xff1a; 三、键盘输入 四、数值运算 为什么…

余集和拉格朗日定理

L&#xff1a;一个群的例子&#xff08;在下面的文章中进一步详细介绍&#xff09;;R&#xff1a;约瑟夫路易拉格朗日&#xff08;1736-1813&#xff09;&#xff0c; 一、说明 数学家总是痴迷于根据乍一看似乎完全无关的事实/观察来形成概括。为什么&#xff1f;原因很简单&am…

ideaSSM图书借阅管理系统VS开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 SSM 图书借阅管理系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码 和数据库&#xff0c;系统主…

JS-11A/11时间继电器 板前接线 JOSEF约瑟

系列型号&#xff1a; JS-11A/11集成电路时间继电器&#xff1b;JS-11A/12集成电路时间继电器&#xff1b; JS-11A/13集成电路时间继电器&#xff1b;JS-11A/136集成电路时间继电器&#xff1b; JS-11A/137集成电路时间继电器&#xff1b;JS-11A/22集成电路时间继电器&#…

一点点金融 4

一点点金融 4 第一性原理&#xff1a;关键事件前后&#xff0c;市场会从不确定性转变为确定性弹簧板、天花板&#xff1a;作为止损、换策略的依据怎么判断弹簧板、天花板&#xff1f; 第一性原理&#xff1a;关键事件前后&#xff0c;市场会从不确定性转变为确定性 在关键事件…

74LVC04六角逆变器-国产兼容MS9113

MS9113S 是一款 S/PDIF 信号接收器。当输入信号频率为 0.1MHz 至 40MHz 时&#xff0c;芯片放大该输入信号至电源电压。最小输入信号幅度的典型值为 80mV。MS9113S 包含一个信号标识位管脚&#xff0c;有输入信号则为高电平&#xff0c;无输入信号则为低电平。MS9113S 还包含一…

LeetCode-94. 二叉树的中序遍历【栈 树 深度优先搜索 二叉树】

LeetCode-94. 二叉树的中序遍历【栈 树 深度优先搜索 二叉树】 题目描述&#xff1a;解题思路一&#xff1a;递归解题思路二&#xff1a;迭代解题思路三&#xff1a;0 题目描述&#xff1a; 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 示例 1&#xff1…

调用飞书获取用户Id接口成功,但是没有返回相应数据

原因&#xff1a; 该自建应用没有开放相应的数据权限。 解决办法&#xff1a; 在此处配置即可。

Redis高可用主从复制与哨兵模式

前言 在生产环境中&#xff0c;除了采用持久化方式实现 Redis 的高可用性&#xff0c;还可以采用主从复制、哨兵模式和 Cluster 集群的方法确保数据的持久性和可靠性。 目录 一、主从复制 1. 概述 2. 作用 3. 主从复制流程 4. 部署 4.1 安装 redis 4.2 编辑 master 节…

基于深度学习的条形码二维码检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)

摘要&#xff1a;本文深入研究了基于YOLOv8/v7/v6/v5的条形码二维码检测系统。核心采用YOLOv8并整合了YOLOv7、YOLOv6、YOLOv5算法&#xff0c;进行性能指标对比&#xff1b;详述了国内外研究现状、数据集处理、算法原理、模型构建与训练代码&#xff0c;及基于Streamlit的交互…

年少不知EFCore好,错把SqlSugar当成宝

背景&#xff1a;依然记得我的第一份WebApi项目使用得是SqlSugar&#xff0c;当时还没有系统学习b/s这边的知识&#xff0c;跟着别人做项目用SqlSugar觉得非常方便&#xff0c;减少了自己手写ADO.Net的痛苦。但是今天发现这个EFCore也是巨好用啊&#xff0c;下面写一下他的简单…

C语言——内存函数

前言&#xff1a; C语言中除了字符串函数和字符函数外&#xff0c;还有一些函数可以直接对内存进行操作&#xff0c;这些函数被称为内存函数&#xff0c;这些函数与字符串函数都属于<string.h>这个头文件中。 一.memcpy&#xff08;&#xff09;函数 memcpy是C语言中的…

LlamaIndex——RAG概述

文章目录 一、使用LLM1. 模型2. 词嵌入3. Prompt 二、加载1. 加载2. 转换&#xff08;1&#xff09;高级API&#xff08;2&#xff09;低级API 三、索引/EmbeddingTop K Retrieval 四、存储五、查询六、评估1. 生成结果质量评估2. 检索结果评估 RAG&#xff08;检索增强生成&am…

【javaScript】DOM编程入门

一、什么是DOM编程 概念&#xff1a;DOM(Document Object Model)编程就是使用document对象的API完成对网页HTML文档进行动态修改&#xff0c;以实现网页数据和样式动态变化的编程 为什么要由DOM编程来动态修改呢&#xff1f;我们就得先理解网页的运行原理&#xff1a; 如上图&a…