likes
comments
collection
share

React diff 算法的流程

作者站长头像
站长
· 阅读数 33

React diff 算法的流程如下:

  1. 比较两棵树的根节点,如果不同,则认为整棵树需要更新。
  2. 对于相同的节点,比较它们的属性和子节点。
  3. 对于同级节点,可以通过唯一 key 标识来判断是否为同一个节点,从而避免不必要的更新。
  4. 递归处理所有子节点。
  5. 对于有 key 的子节点,React 会尝试在旧的子节点中查找是否存在与之对应的节点。如果找到了,则将新节点复用旧节点,并对其进行更新;如果没有找到,则将新节点插入到相应位置。
  6. 对于新增的节点,直接插入到相应位置。
  7. 对于旧节点中已经不存在的节点,直接删除。
  8. 在比较过程中,React 会尽可能地复用已有的节点,以最小化 DOM 操作的次数。同时,React 还提供了一些优化手段,如 shouldComponentUpdate 和 React.memo,让开发者可以在需要时自定义组件更新的逻辑和条件。

整个过程中,React 通过记录每个节点的状态和变化情况,最终生成一组变更操作(比如要插入、删除、更新哪些节点),并一次性批量地执行这些操作,以提高性能。

在实际应用中,React diff 算法还会针对一些特殊情况进行优化。比如:

  1. 对于同级节点的移动操作,React 会尽可能地减少移动的次数,以提高性能。
  2. 对于同级列表的渲染,React 会使用key属性来判断是否需要更新,从而避免因为顺序变化导致的不必要更新。
  3. 对于大型列表的渲染,建议采用 “虚拟滚动” 技术,只渲染屏幕内可见的部分,从而提高性能和用户体验。

总的来说,React diff 算法是 React 中非常重要的一部分,它通过智能地判断哪些节点需要更新、哪些节点可以复用等方式,使得 React 应用在处理大量数据时依然能够保持良好的性能表现。

React diff 算法中,单节点和多节点的区别

在 React diff 算法中,单节点和多节点的区别主要体现在更新策略上。

 /**
   * 比较子Fibers  DOM-DIFF 就是用老的子fiber链表和新的虚拟DOM进行比较的过程
   * @param {*} returnFiber 新的父Fiber
   * @param {*} currentFirstChild 老fiber第一个子fiber   current一般来说指的是老
   * @param {*} newChild 新的子虚拟DOM  h1虚拟DOM
   */
  function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
    //现在需要处理更新的逻辑了,处理dom diff
    //考虑新的节点只有一个的情况
    if (typeof newChild === "object" && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild));
        default:
          break;
      }
    }
    //newChild [hello文本节点,span虚拟DOM元素]
    if (isArray(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
    }
    return null;
  }
  return reconcileChildFibers;
}

对于单节点,React diff 算法会直接比较其内容是否有变化,如果有变化,则直接更新该节点的内容,而不会进行进一步的子节点比较。这是因为单节点没有子节点,所以不存在子节点在顺序、位置等方面的变化,因此可以省略掉一些比较和操作。

 /**
   * 
   * @param {*} returnFiber 根fiber div#root对应的fiber
   * @param {*} currentFirstChild 老的FunctionComponent对应的fiber
   * @param {*} element 新的虚拟DOM对象
   * @returns 返回新的第一个子fiber
   */
  function reconcileSingleElement(returnFiber, currentFirstChild, element) {
    //新的虚拟DOM的key,也就是唯一标准
    const key = element.key;        // null
    let child = currentFirstChild; //老的FunctionComponent对应的fiber
    while (child !== null) {
      //判断此老fiber对应的key和新的虚拟DOM对象的key是否一样 null===null
      if (child.key === key) {
        //判断老fiber对应的类型和新虚拟DOM元素对应的类型是否相同
        if (child.type === element.type) {// p div
          deleteRemainingChildren(returnFiber, child.sibling);
          //如果key一样,类型也一样,则认为此节点可以复用
          const existing = useFiber(child, element.props);
          existing.ref = element.ref;
          existing.return = returnFiber;
          return existing;
        } else {
          //如果找到一key一样老fiber,但是类型不一样,不能此老fiber,把剩下的全部删除
          deleteRemainingChildren(returnFiber, child);
        }
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    //因为我们现实的初次挂载,老节点currentFirstChild肯定是没有的,所以可以直接根据虚拟DOM创建新的Fiber节点
    const created = createFiberFromElement(element);
    created.ref = element.ref;
    created.return = returnFiber;
    return created;
  }
//将删除的节点放入returnFiber.deletions
  function deleteChild(returnFiber, childToDelete) {
    if (!shouldTrackSideEffects)
      return;
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete]
      returnFiber.flags |= ChildDeletion;
    } else {
      returnFiber.deletions.push(childToDelete);
    }
  }



对于多节点,React diff 算法需要逐层比较其子节点的内容和状态,并根据变化情况采取不同的更新策略。具体来说,React diff 算法会通过比较每个子节点的 “key” 属性来确定它们的唯一性和相对位置,然后尽可能地复用已有的节点,并最小化 DOM 操作的次数。如果出现了新增、删除、移动等操作,则会相应地执行插入、删除、移动等操作,从而实现整个容器节点的更新。


// 第一轮循环 删除没有复用的老节点
//  使用lastPlacedIndex 来判断是否移动节点
//如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动


function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
    let resultingFirstChild = null; //返回的第一个新儿子
    let previousNewFiber = null; //上一个的一个新的儿fiber
    let newIdx = 0;//用来遍历新的虚拟DOM的索引
    let oldFiber = currentFirstChild;//第一个老fiber
    let nextOldFiber = null;//下一个第fiber
    let lastPlacedIndex = 0;//上一个不需要移动的老节点的索引
    // 开始第一轮循环 如果老fiber有值,新的虚拟DOM也有值
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      //先暂下一个老fiber
      nextOldFiber = oldFiber.sibling;
      //试图更新或者试图复用老的fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (newFiber === null) {
        break;
      }
      if (shouldTrackSideEffects) {
        //如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DOM,那就删除老fiber,在提交阶段会删除真实DOM
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }
      //指定新fiber的位置, 确定节点是否可以复用,移动或者替换
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C)
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber
    }
    //新的虚拟DOM已经循环完毕,3=>2
    if (newIdx === newChildren.length) {
      //删除剩下的老fiber
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    if (oldFiber === null) {
      // 处理完需要删除的节点
      //如果老的 fiber已经没有了, 新的虚拟DOM还有,进入插入新节点的逻辑
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx]);
        if (newFiber === null) continue;
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        //如果previousNewFiber为null,说明这是第一个fiber
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //这个newFiber就是大儿子
        } else {
          //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
          previousNewFiber.sibling = newFiber;
        }
        //让newFiber成为最后一个或者说上一个子fiber
        previousNewFiber = newFiber;
      }
    }
    // 开始处理移动的情况
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //开始遍历剩下的虚拟DOM子节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          //如果要跟踪副作用,并且有老fiber
          if (newFiber.alternate !== null) {
            existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
          }
        }
        //指定新的fiber存放位置 ,并且给lastPlacedIndex赋值
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber; //这个newFiber就是大儿子
        } else {
          //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面
          previousNewFiber.sibling = newFiber;
        }
        //让newFiber成为最后一个或者说上一个子fiber
        previousNewFiber = newFiber;
      }
    }
    if (shouldTrackSideEffects) {
      //等全部处理完后,删除map中所有剩下的老fiber
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }
    return resultingFirstChild;
  }



  function placeChild(newFiber, lastPlacedIndex, newIdx) {
    //指定新的fiber在新的挂载索引
    newFiber.index = newIdx;
    //如果不需要跟踪副作用
    if (!shouldTrackSideEffects) {
      return lastPlacedIndex;
    }
    //获取它的老fiber
    const current = newFiber.alternate;
    //如果有,说明这是一个更新的节点,有老的真实DOM。
    if (current !== null) {
      const oldIndex = current.index;
      //如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动
      if (oldIndex < lastPlacedIndex) {
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        return oldIndex;
      }
    } else {//如果没有,说明这是一个新的节点,需要插入
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
  }

总的来说,单节点和多节点虽然都遵循 React diff 算法的基本流程,但在具体的更新策略上存在一些差异,需要针对不同的场景进行优化处理。

除了更新策略上的差异,单节点和多节点在渲染性能上也有一些区别。####

对于单节点,由于它没有子节点,所以不需要递归调用 diff 算法,因此处理速度很快,渲染性能比较高。

对于多节点,由于它需要逐层比较子节点的状态和位置,并可能涉及到插入、删除、移动等操作,所以需要进行递归调用 diff 算法,并且随着节点数量的增加,处理速度会逐渐变慢。因此,在大规模列表渲染等场景下,需要采取一定的优化措施,如使用虚拟滚动等技术,以提高渲染性能和用户体验。

总的来说,单节点和多节点都是 React 应用中常见的节点类型,在实际应用中需要根据具体情况选择合适的节点类型,并针对不同的场景进行优化处理,以保证应用的渲染性能和用户体验。

diff中lastPlaceIndex的作用

在 React diff 算法中,lastPlaceIndex(也称为“上一个节点的位置索引”)是一个用于优化节点移动操作的重要参数,它记录了当前正在遍历的子节点列表中,上一个参与比较的节点在原先列表中的位置。

具体来说,当 React diff 算法在比较两个相邻的子节点时,会将上一个子节点的 lastPlaceIndex 与当前子节点的位置进行比较。如果当前子节点的位置小于等于上一个子节点的位置,则认为这两个子节点之间的距离没有变化,不需要进行移动操作;否则,会将当前子节点插入到正确的位置,并将上一个子节点从原先位置删除,以实现节点在列表中的移动操作

通过使用 lastPlaceIndex 参数,React diff 算法可以快速判断节点的移动情况,并最小化 DOM 操作的次数,从而提高应用的渲染性能。同时,为了避免 lastPlaceIndex 参数过期或失效导致的错误比较结果,React diff 算法会在处理完每个子节点后更新 lastPlaceIndex 的值,以确保其准确性和有效性。

转载自:https://juejin.cn/post/7213695693201260600
评论
请登录