React 的源码与原理解读(六):reconcileChildren 与 DIFF 算法

news2024/10/6 20:32:07

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

本节的我们将从 上一节留下的问题出发,谈谈 reconcileChildren() 中怎么样最终生成 fiber 结点,其中我们会谈到 React 核心的 DIFF 算法,他的核心 —— 复用怎么实现,同时他是怎么样把比较的时间复杂度进行了优化

reconcileChildren

上一节中我们讲到,beginWork 到最后调用了reconcileChildren 这个函数来处理 ,而 reconcileChildren 这个函数中将调用我们耳熟能详的 DIFF 算法来处理我们的 element 生成 fiber ,那么这篇我们从这个函数开始,它在代码的这个位置:

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberBeginWork.js

这是它的主要逻辑,其实就是根据是不是第一次渲染来调用不同的函数,我们可以看到 current === null 这里逻辑又一次出现了:

export function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes) {
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

我们来看看这两个函数的定义,可以看到,他们都是 ChildReconciler 函数生成的,只是传入的参数不同

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

我们来看这个 ChildReconciler 函数,它的代码非常长,我们先看他的返回值,他返回了一个 reconcileChildFibers

function ChildReconciler(shouldTrackSideEffects) {
  return reconcileChildFibers;
}

reconcileChildFibers 是一个函数,他定义在 ChildReconciler 函数内部,他的逻辑如下:

  • 首先判断是不是 fragment 元素,如果是则使用其孩子,Fragment 组件是 React 16.2 中新增的特性,它能够在不额外创建 DOM 元素的情况下,让 render()方法中返回多个元素,因而我们处理它的时候,需要无视这个标签
  • 之后我们处理传入的节点,如果是一个对象,那么它是一个 element 元素,分为普通元素,Lazy 类型、Portal 类型,节点数组,其他五种类型;如果传入的一个 string 或者 number 节点,那么作为一个文本节点来处理
  • 要提到的是如果传入的内容不匹配任何一项内容,那么说明它可能是 boolean, null, undefined 等,不能转化为 Fiber 节点
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes, 
  ): Fiber | null {
    // 判断是不是 fragment
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
        
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }
    // 判断该节点的类型
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          // 一般的React组件,
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          // portal类型 
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          // lazy类型 
          const payload = newChild._payload;
          const init = newChild._init;
          return reconcileChildFibers(
            returnFiber,
            currentFirstChild,
            init(payload),
            lanes,
          );
      }
	  // newChild 是一个数组
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
	  // 其他迭代类型,跟数组类似,只是遍历方式不同
      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
      throwOnInvalidObjectType(returnFiber, newChild);
    }
    // 文本节点
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType(returnFiber);
      }
    }

    //说明 newChild 可能是boolean, null, undefined等类型,不能转为fiber节点。直接从删除所有旧的子 Fiber (不继续比较了)
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

函数 reconcileChildFibers 处理我们上一节中放入的 element 结构,他处理这个 element 的核心的将它和一层中元素进行比较,根据规则判断这点节点能不能复用,我们都知道,一个 Fiber 节点含有一个 sibing 指针,通过sibing 指针,我们可以找到一个节点所有的兄弟,从而遍历整个同一层的元素。

我们来具体看看比较的过程:

reconcileSingleElement

reconcileSingleElement 用于处理一般的React组件,比如函数组件、类组件、html 标签等,我们来看看它的代码,他的判断逻辑是这样的:

  • 首先提取出当前 element 的 key 属性,找到在 Fiber 树同一层中,有没有和他一个 key 的元素
  • 之后我们判断这个 key 相同的元素和当前元素的类型是不是一致,如果一致则复用这个元素
  • 如果没有匹配元素则创建一个新的节点
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  //当前节点的key
  const key = element.key;
  let child = currentFirstChild;
  // 循环检测一层中和当前节点的 key
  while (child !== null) {
    // 找到 key 相等的元素
    if (child.key === key) {
      const elementType = element.type;
      //元素类型相等
      if (child.key === key) {
        const elementType = element.type;
         //  REACT_FRAGMENT_TYPE,特判
        if (elementType === REACT_FRAGMENT_TYPE) {
          if (child.tag === Fragment) {
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其children
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            //Fragment没有 ref属性
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        } else {
          if (
            child.elementType === elementType ||
            (__DEV__
              ? isCompatibleFamilyForHotReloading(child, element)
              : false) ||
            (typeof elementType === 'object' &&
              elementType !== null &&
              elementType.$$typeof === REACT_LAZY_TYPE &&
              resolveLazy(elementType) === child.type)
          ) {
            deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用Fiber子节点且确认只有一个子节点,因此标记删除掉该child节点的所有sibling节点
            const existing = useFiber(child, element.props); // 复用 child 节点和 element.props 属性
            existing.ref = coerceRef(returnFiber, child, element); // 处理ref
            existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            return existing;
          }
        }
      // key一样,类型不同,直接删除该节点和其兄弟节点
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // 若key不一样,不能复用,标记删除当前单个child节点
      deleteChild(returnFiber, child);
    }
    // 指针指向下一个sibling节点
    child = child.sibling; 
  }
  // 创建一个新的fiber节点
  if (element.type === REACT_FRAGMENT_TYPE) {
    //  REACT_FRAGMENT_TYPE,特判
    const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key);
    created.return = returnFiber; // 新节点的 return 指向到父级节点
    // FRAGMENT 节点没有 ref
    return created;
  } else {
    // 普通的html元素、函数组件、类组件等
    // 从 element 创建 fiber 节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理ref
    created.return = returnFiber;
    return created;
  }
}

我们注意到,复用一个节点调用了我们的 useFiber 函数,我们再来看看这个函数:

他调用了 createWorkInProgress 克隆了一个 Fiber 节点,放到我们的 WorkInProgress 树中,之前我们提过我们的 React 中有两棵树,FiberRootNode 节点中的 current 指针指向到哪棵树,就展示那棵树,我们把当前正在展示的那棵树叫做 current,将要构建的那个叫做 workInProgress,通过 alternate 属性进行互相的指向。这里我们使用 current 树来创建我们的 workInProgress 树:

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
  // 调用了 createWorkInProgress 这个函数来克隆一个节点
  const clone = createWorkInProgress(fiber, pendingProps);
  clone.index = 0;
  clone.sibling = null;
  return clone;
}

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  // workInProgress 是空的,也就是初始化的时候,创建一个新节点
  if (workInProgress === null) {
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // workInProgress 和 current通过 alternate 属性互相进行指向
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;
    workInProgress.flags = NoFlags;
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;

    if (enableProfilerTimer) {
      workInProgress.actualDuration = 0;
      workInProgress.actualStartTime = -1;
    }
  }

  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
    
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  if (enableProfilerTimer) {
    workInProgress.selfBaseDuration = current.selfBaseDuration;
    workInProgress.treeBaseDuration = current.treeBaseDuration;
  }

  return workInProgress;
}

最后我们看到我们调用了 createFiberFromElement 函数来从 element 创建了一个 Fiber ,而这个函数调用了 createFiberFromElement 函数进行处理,我们来看看这个函数的逻辑:

  • 它首先根据通过 type 属性区别组件、html 节点和其他类型组件
  • 如果是组件则根据 shouldConstruct 判断我们它是函数组件还是类组件,因为类组件都是要继承 React.Component 的,而 React.Component 的 prototype 上有一个 isReactComponent 属性,值为{},由此可以进行判断
  • 之后我们调用 createFiber 函数来创建一个 Fiber 节点,这个函数我们在之前的教程中使用过,已经忘记的读者可以翻一翻之前的教程,获得这个创建好的Fiber 之后我们将 type 等属性赋值给他然后返回
  • 这个创建出来的 Fiber 最后将被放到我们的树中
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  if (__DEV__) {
    owner = element._owner;
  }
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props; 
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

export function createFiberFromTypeAndProps(
  type: any, 
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let fiberTag = IndeterminateComponent; // 我们还不知道当前fiber是什么类型
  let resolvedType = type;
  if (typeof type === 'function') {
    // 当前是函数组件或类组件
    if (shouldConstruct(type)) {
      fiberTag = ClassComponent;
    } else {
      // 函数组件
    }
  } else if (typeof type === 'string') {
    // type是普通的html标签
    fiberTag = HostComponent;
  } else {
    // 其他类型,按下不表
  }
  // 调用 createFiber() 函数,生成 fiber 节点
  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type; // fiber中的 elmentType 与 element 中的 type 一样,
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  return fiber;
}

function shouldConstruct(Component: Function) {
  // 类组件都是要继承 React.Component 的,而 React.Component 的 prototype 上有一个 isReactComponent 属性,值为{}
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

reconcileSingleTextNode

对于一个纯 html 文本,处理就相对简单,当然他也分多文本的数组和单个节点两种情况,这里我们先讲单个节点的处理,数组类型的处理和多个 ReactElement 元素的处理类似,我们之后会详细说明:

他的处理在 reconcileSingleTextNode 这个函数中,我们不再需要判定 key,因为文本节点没有 key 属性;在比较当前一层的数据时,因为文本节点只有一个节点,没有兄弟节点,所以只有当 current 的第一个结点是文本节点时才能复用,否则就删除所有元素。

// 调度文本节点
function reconcileSingleTextNode(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  textContent: string,
  lanes: Lanes,
): Fiber {
  // 不再判断文本节点的key,因为文本节点就来没有key
  if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
    // 若当前节点是文本,则直接删除后续的兄弟节点
    deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
    const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本
    existing.return = returnFiber;
    return existing;
  }
  // 若不存在子节点,或者第一个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点
  deleteRemainingChildren(returnFiber, currentFirstChild);
  const created = createFiberFromText(textContent, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}

reconcileChildrenArray

当我们需要处理的元素不是单个数据,而是一组数据的时候,比如一个 div 嵌套了 三个 p 标签,这更加常见,我们需要调用 reconcileChildrenArray 进行处理,我们来看看他的逻辑:

已知在一个数组中元素的更新可能存在一下几种情况:

  • 新序列和旧序列相比,元素出现的位置相同
  • 新序列中新增了元素
  • 新序列中删除了元素
  • 新序列和旧序列都出现的元素,但是元素出现顺序不同

我们先按照顺序遍历 Fiber 链表和我们的数组,因为我们的 Fiber 链表之间是通过 sibing 指针指向下一个节点的,但是没有回到上一个的指针,所以我们只能从前往后遍历我们的链表,同时我们可以会看一下,在 Fiber 中我们有这样一个属性——index,他标记了该元素在 Fiber 兄弟链表中的位置,这个属性将在这个地方排上用场:

let resultingFirstChild: Fiber | null = null; // 用于返回的链表
let previousNewFiber: Fiber | null = null; 

let oldFiber = currentFirstChild; // 旧 Fiber 链表的节点,开始指向同一层中的第一个节点
let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 长度
let newIdx = 0; // 表示遍历 newChildren 的索引指针
let nextOldFiber = null; // 下一个 fiber 节点

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  // 如果旧的节点大于新的
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    // 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点
    nextOldFiber = oldFiber.sibling;
  }

  // 比较旧的节点和将要转换的 element 
  const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);
  // 匹配失败,不能复用
  if (newFiber === null) {
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }
  if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
      // newFiber 不是基于 oldFiber 的 alternate 创建的,销毁旧节点
      deleteChild(returnFiber, oldFiber);
    }
  }
  // 更新lastPlacedIndex
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

  // 更新返回的链表
  if (previousNewFiber === null) {
    // 若整个链表为空,则头指针指向到newFiber
    resultingFirstChild = newFiber;
  } else {
    // 若链表不为空,则将newFiber放到链表的后面
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber; 
  oldFiber = nextOldFiber; // 继续下一个节点
}

updateSlot 函数用于判定在相同的对应位置的两个元素是不是能够复用,它判断的依据还是两个元素的 key 是不是相同

function updateSlot(returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes): Fiber | null {
  // 若key相等,则更新fiber节点;否则直接返回null
  const key = oldFiber !== null ? oldFiber.key : null;
  if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') {
    // 文本节点本身是没有key的,若旧fiber节点有key,则说明无法复用
    if (key !== null) {
      return null;
    }
    // 若旧fiber没有key,即使他不是文本节点,我们也尝试复用
    return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
  }
  if (typeof newChild === 'object' && newChild !== null) {
    // 若是一些ReactElement类型的,则判断key是否相等;相等则复用;不相等则返回null
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        if (newChild.key === key) {
          // key一样才更新
          return updateElement(returnFiber, oldFiber, newChild, lanes);
        } else {
          // key不一样,则直接返回null
          return null;
        }
      }
      // 省略....
    }
    if (isArray(newChild) || getIteratorFn(newChild)) {
      // 当前是数组或其他迭代类型,本身是没有key的,若oldFiber有key,则无法复用
      if (key !== null) {
        return null;
      }
      // 若 newChild 是数组或者迭代类型,则更新为fragment类型
      return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
    }
  }
  // 其他类型不进行处理,直接返回null
  return null;
}

如果循环结束之后,旧的链表还没遍历完,说明剩下的节点已经不需要了,直接删除即可

// 遍历结束(访问相同数量的元素了)
if (newIdx === newChildren.length) {
  // 删除旧链表中剩余的节点
  deleteRemainingChildren(returnFiber, oldFiber);
  // 返回新链表的头节点指针
  return resultingFirstChild;
}

如果经过上面操作后,旧的 Fiber 用完了,但 element 元素没有全部访问到,说明剩下的元素没有对应的可以复用的节点,直接新建节点即可,createChild 的逻辑和 updateSlot 基本一致,只是不用考虑复用的问题:

// 若旧数据中所有的节点都复用了
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;
}

如果新旧元素都没遍历完,说明出现了元素乱序的情况,我们需要把旧节点放到 Map 中,然后根据 key 或者 index 获取。

//如果新旧元素都没遍历完, mapRemainingChildren 生成一个以 oldFiber 的 key 为 key, oldFiber 为 value 的 map
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
  // 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点
  const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes);
  if (newFiber !== null) {
    if (shouldTrackSideEffects) {
      if (newFiber.alternate !== null) {
         //newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点,这里我们要在 map 中删除,因为后面会把 map 中剩余未复用的节点删除掉的
        existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 接着之前的链表进行拼接
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}

if (shouldTrackSideEffects) {
  // 将 map 中没有复用的 fiber 节点添加到删除队列中,等待删除
  existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// 返回新链表的头节点指针
return resultingFirstChild;

diff 算法

ok,我们刚刚已经阅读了一遍 reconcileChildren 所作的操作,现在我们来整理一下它的处理逻辑,将它抽象成一个用于更新我们的 Fiber 树的算法。这个算法就是我们 React 的 DIFF 算法,我们先来谈谈 React 的 DIFF 的大前提:

  • 在 web 开发中,很少会有跨层级的元素变化,,比如现在我有一个 p 标签,我经常做的操作是:改变这个标签的内容,或者在 p 标签的同一级再插入一个 p 标签,我们很少有在更新页面的时候在 p 标签的外围嵌套一个 div 这样的操作。
  • 两个不同类型的组件会产生两棵不同的树形结构
  • React 中的 key 是唯一的,一个 key 可以唯一标识一个元素

基于这样的前提,React 给出的策略是这样的:

React 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。对比时,每次都选择同一个父节点的孩子进行比较,如下图,同一个父亲节点下的所有孩子会进行比较

请添加图片描述

如果出现的跨层移动的情况,那么在父亲节点的比较时,就会删除跨层移动的节点,比如下图,在对root 进行比较的时候,A已经被标记为删除了,之后在对 B 进行比较的时候,虽然组件 A 出现了,但是我们并不会复用它,而会新建一个组件A

在这里插入图片描述

React 对于不同类型的组件,默认不需要进行比较操作,直接重新创建。对于同类型组件,使用 diff 策略进行比较,比如下图:两个组件的根节点不同,也就是说不是一个组件,但是组件的内容相同,这种情况下,React 并不会进行复用,而是直接新建:

在这里插入图片描述

对于位于同层的节点,通过唯一 key 来判断老集合中是否存在相同的节点,如果没有则创建;如果有,则判断是否需要进行移动操作。整个操作分为:删除、新增、移动 三种。如下图:我们对 A 进行了移动,对 C 进行了删除,对 E 进行了 新增。

在这里插入图片描述
在这里插入图片描述

经过上述逻辑的优化,React 把经典 DIFF 算法(暴力递归比较)的 O( n^3 ) 优化到了近乎 O( n ) 。

现在你可以再回顾一下我们刚刚讲解的源码,现在你应该对于源码有了新的认识,其中几个可能大部分人第一次读源码的时候不清楚的问题也得到了解决:

  • 为什么在处理一个节点的时候,如果它无法解析(null,undefined),要直接删掉所有元素:因为它已经无法解析了,说明它不需要老节点提供给它的复用,那么老元素的唯一同一层的都没有存在的价值了,全部删掉即可

  • 为什么在处理一个节点的时候找到可以复用的节点要删除剩余节点:因为如果是处理单个节点,那么在同一层中,它只需要一个节点来复用即可,如果能找到复用的节点,剩下的节点都不需要了,直接删除即可

  • 为什么找到一个 key 一样但是类型不一样的元素,要直接删除所有的元素,因为 key 是唯一的,如果 key 相同但是类型不一样,说明是一个不一样的组件,那么调用 两个不同类型的组件会产生两棵不同的树形结构 原则,直接删除即可

  • 这里需要补充的一点是,我们处理数组类型元素的时候,我们使用的比较方法,分别是优先复用节点,然后处理删除、新增和移动,因为在实际的 React 开发过程中,复用节点、新增和删除的出现频率远高于移动,我们最后处理移动的逻辑

总结

这一节中,我们最终讲到了 reconcileChildren() 函数的处理,它通过使用 DIFF 算法来判断哪些节点可以复用,其核心是:

  • 只比较位于同一层的元素
  • 根节点不同就视为整棵树都不同
  • 使用唯一的 key 来标识元素和查找是否可以复用

对此我们对于单个节点,比较同一层是否有可以复用的节点;对于一组节点,我们于同一层比较,判断每个元素应该复用、删除、新增还是移动

判定完成后,我们为每个 React element 节点创建出一个 Fiber 节点上,其或调用 createWorkInProgress 克隆了一个 Fiber ,或者新建一个 Fiber ,新生成的 Fiber 会放到 WorkInProgress 树中,我们把当前正在展示的那棵树叫做 current,将要构建的那个叫做 workInProgress,通过 alternate 属性进行互相的指向。

那么在之后的章节中,我们只剩下最后一步需要处理了,就是我们需要将我们的本次对于虚拟 DOM 的更新同步到我们的真实 DOM 上,展现给用户,那么一次 React 的渲染就完成了,这个阶段被成为 Commit 阶段,下一节我们将详细讲讲它。

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

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

相关文章

常年不卷,按时下班,工作能力强,同事求助知无不言,不扯皮,不拉帮结派,这样的职场清流竟然被裁掉了!...

在职场上&#xff0c;你永远想不到什么样的员工会被优化&#xff0c;比如下面这位&#xff1a;常年不卷&#xff0c;按时下班&#xff0c;工作很专业&#xff0c;同事问什么都回答&#xff0c;不扯皮&#xff0c;不拉帮结派&#xff0c;简直是职场清流。在上个月竟然被优化了&a…

一分钟腾讯云轻量应用服务器性能评测(慎入坑)

腾讯云轻量应用服务器性能评测&#xff0c;轻量服务器CPU主频、处理器型号、公网带宽、月流量、Ping值测速、磁盘IO读写及使用限制&#xff0c;轻量应用服务器CPU内存性能和标准型云服务器CVM处于同一水准&#xff0c;所以大家不要担心轻量应用服务器的性能&#xff0c;腾讯云百…

JavaEE企业级应用开发教程——第十章 初识Spring MVC框架(黑马程序员第二版)(SSM)

第十章 初识Spring MVC框架 JSP Model2架构模型是一种将页面显示、流程控制和业务逻辑分离的Web应用程序架构模型&#xff0c;采用JSP、Servlet和JavaBean技术实现。但是&#xff0c;它将通用逻辑以硬编码的方式实现&#xff0c;每次开发新的Web应用程序时都需要重新编写Servl…

MyBatis注解开发---实现增删查改和动态SQL

目录 1. 环境搭建 &#xff08;1&#xff09;创建持久层接口&#xff0c;并在接口方法上定义Sql语句 &#xff08;2&#xff09;测试方法 &#xff08;3&#xff09;运行结果 2. 注解实现增删查改 &#xff08;1&#xff09;增加用户 &#xff08;2&#xff09;删除用…

【4.17】贪心算法入门

什么是贪心&#xff1f; 贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 刷题或者面试的时候&#xff0c;手动模拟一下感觉可以局部最优推出整体最优&#xff0c;而且想不到反例&#xff0c;那么就试一试贪心。 贪心的解题步骤&#xff1f; 贪心算法一…

《人体地图》笔记

《人体地图》 坂井建雄 著 孙浩 译 腹部通向大腿的隧道 腹部与大腿的分界点是大腿根部&#xff0c;即是腹股沟。 腹壁肌肉连结在腹股沟韧带上&#xff0c;腹壁肌肉包括三层&#xff0c;分别为腹外斜肌、腹内斜肌和腹横肌&#xff0c;每块肌肉都有一个张开的小孔&#xff0c;…

【靶场设计和渗透】

目录 一、前言 二、靶场设计 1、局域网 2、说明 三、渗透测试 1、信息收集 2、漏洞利用 四、后渗透利用 1、提权 2、权限维持 一、前言 为了深入贯彻学习网络安全法律法规&#xff0c;深入学习渗透测试知识&#xff0c;强化实战技能............ 编不出来了&#xff…

The Sandbox 的 OliveX Fitness 之城来啦!

4 月 11 日至 17 日&#xff0c;亲自来体验一下吧&#xff01; 这种独特的体验将有趣和故事驱动的游戏与健身以及奖励结合起来。玩家可以探索隐藏的角落&#xff0c;逃出迷宫&#xff0c;爬上梯子&#xff0c;清除障碍&#xff0c;完成相互关联的任务&#xff0c;所以战略规划是…

Avue dynamic表单实现form单选,修改及新增项

Avue dynamic表单实现form单选&#xff0c;修改及新增项 AvueDialogFormTableViewOption.js /** Description:银行账号* Version: 1.0* Autor: Tj* Date: 2023-03-21 11:02:42*/ export const BankAccountOption (vueObj, formData) > {return {labelWidth: 100, //整体列…

【华为OD机试】1046 - 计算字符串的编辑距离

文章目录一、题目&#x1f538;题目描述&#x1f538;输入输出&#x1f538;样例1二、思路解析三、代码参考作者&#xff1a;KJ.JK&#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f…

GDPU C语言 天码行空8

1. 求序列和 ⭐ 输出没有小数的浮点数 #include <stdio.h>double fun(int a, int n) {double res 0;int aa a,i;//aa 记录每一项for(i 1; i < n; i){res aa;aa aa * 10 a;}return res; }int main(){int a,n;scanf("%d %d", &a,&n);printf(&q…

Linux学习笔记——HTTPS协议

文章目录HTTPS是什么什么是加密为什么要进行加密常见的加密方式数据摘要与数据指纹数字签名HTTPS的工作过程探究方案1—只使对称加密方案2—只使非对称加密方案3—双方都是用非对称加密方案4—非对称加密对称加密中间人攻击证书方案5—非对称加密对称加密证书认证HTTPS是什么 H…

hypack单波束采集和处理基本流程

前两天有个读者问是否有单波束测深的操作和处理的步骤&#xff0c;在xiaok海洋测绘网上用关键字“单波束”搜索的结果就是确实没有相关的文章&#xff0c;相关的文章都是外业过程记录。下面以单波束XX&#xff08;可以是任何单波束&#xff09;和采集处理软件Hypack为例说明单波…

树莓派通过网线连接笔记本实现笔记本电脑Wifi的网络共享

基于windows电脑连接树莓派进行设置&#xff1a;通过通过一根网线&#xff0c;连接树莓派和电脑&#xff0c;使电脑和树莓派构成一个局域网&#xff0c;然后树莓派接收来自笔记本电脑wifi网络的共享网络。操作方法类似台式机通过网线共享笔记本电脑无线网络的步骤 1、 保证笔记…

[c++整人代码]超级加倍,让人承认自己是大傻猪

㊀程序介绍 这是本人看到的一个整人小病毒&#xff0c;唯一的杀伤力就是逼着你想坑的人承认他是猪。 本次更新&#xff1a;要求运行程序的人手动输入“我是猪”三个字 ㊁程序截图 1 本程序的窗口会自动保持最前 2 无法关闭本窗口 ㊂代码展示&#xff1a; #include <Wi…

【pinia持久化存储】使用pinia和pinia-plugin-persistedstate依赖进行数据的持久化存储

简言 使用pinia和pinia-plugin-persistedstate依赖进行数据的持久化存储。 存储方式 &#xff1a; localStoragesessionStorage pinia-plugin-persistedstate 中文官网 pinia 中文官网 安装 安装和使用 pinia &#xff0c;请参考使用pinia文章。 安装 pinia-plugin-pers…

使用PDF猫怎么将PNG图片转化成JPG格式图片?

如需了解更多办公应用的相关教程&#xff0c;可到赛效官方网站的应用资讯栏目或者应用问答栏目下查看更多。 TXT文本工具是微软操作系统上附带的一种文本格式&#xff0c;打开速度比较快&#xff0c;且记录文字内容时比较便捷且快速&#xff0c;但是很多时候记录成TXT文本的工…

八大排序算法(冒泡排序、快速排序、堆排序.....)

每坚持一天&#xff0c;offer就会离我更近一步&#x1f339; 文章目录冒泡排序选择排序插入排序希尔排序快速排序计数排序堆排序归并排序冒泡排序 算法描述&#xff1a;从第一个元素开始&#xff0c;两两比较&#xff0c;如果前者比后者大&#xff0c;那么就将两者进行交换&am…

[oeasy]python0133_[趣味拓展]颜文字_流石兄弟_表情文字_2ch_kaomoji

颜文字 回忆上次内容 上次我们了解unicode 里面有各种字体 甚至还有emoji emoji 本质上也是文字 按照unicode的方式编码存储时按照utf-8的方式编码显示时按照系统定义的方式进行显示 还有什么好玩的亚文化吗&#xff1f;&#x1f914; emoticon 1982 年 9 月 19 日 诞生了第…

docker安装rabbitmq的延迟队列插件

1.进入rabbitmq镜像 docker exec -it rabbitmq bash2.查看rabbitmq版本号&#xff0c;方便查找对应版本的延迟队列插件 rabbitmqctl version2.查询rabbitmq插件列表 rabbitmq-plugins list若没有rabbitmq_delayed_message_exchange-xxx.ez&#xff0c;则可以去[延迟队列插件…