Bootstrap

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

写在专栏开头(叠甲)

  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 阶段,下一节我们将详细讲讲它。

;