React16源码: React中的reconcileChildIterator和reconcileChildrenArray的源码实现

reconcileChildIterator 和 reconcileChildrenArray

1 )概述

  • 在react更新某一个节点的时候,要根据这个节点,它的类型去获取它的children
  • 比如说如果是 Function Component,它要调用这个 component 计算出它的return的属性
  • return的属性可能是一个数组,可能是单个的 ReactElement,可能是 number, string 这些类型
  • 要根据这些不同的类型去进行一些特殊的处理,然后得到它的children,和对应的 fiber 对象
  • 这样的话,又可以继续往下去迭代,迭代到最终的一个节点
  • 在经过这个过程,就可以把一个 fiber 树的一侧的子树进行完整的遍历和构建的过程
  • 在这个过程当中,有一个 children 的类型比较特殊,那就是数组
  • 在我们使用数组作为children返回的时候,都会看到这样的一个提醒
  • 那就是我们每一个节点上面必须要有一个key属性
  • 如果这个key属性没有,那 react 就会给我们一个提醒
  • 我们必须要对数组中的每一个节点使用一个唯一的key来标识它
  • 这个key和数组调和有特定的关系,主要关注:
    • A. key的作用
    • B. 对比数组 children 是否可复用的一个过程
    • C. generator 和 array遍历的区别

2 )源码

在 reconcileChildFibers 函数中调用的 reconcileChildrenArray

定位到 packages/react-reconciler/src/ReactChildFiber.js#L732

先看下 reconcileChildrenArray 这个API

新老children的对比过程,以及判断节点是否可复用的过程,这个过程会涉及到react的一个算法

尽量减少 数组的遍历次数,达到复用节点的过程

js 复制代码
// 对于 newChildren 的遍历本质上是 O(n)
// 第一次遍历通过 newIdx++ 的方式,后面 特殊情况的判断来加速性能提升
// 只有在最后一个 for 循环中进行完整遍历,完成后创建节点
// 这里有很多判断一个节点是否可复用,都是通过 key 是否存在来判断的
// 如果不服用并且创建新的 fiber 节点,删除老的 fiber 节点 会导致内存申请和内存回收频繁
// 整体对性能存在影响,因为过于频繁的内存回收会导致内存抖动的问题
function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  expirationTime: ExpirationTime,
): Fiber | null {
  // This algorithm can't optimize by searching from boths ends since we
  // don't have backpointers on fibers. I'm trying to see how far we can get
  // with that model. If it ends up not being worth the tradeoffs, we can
  // add it later.

  // Even with a two ended optimization, we'd want to optimize for the case
  // where there are few changes and brute force the comparison instead of
  // going for the Map. It'd like to explore hitting that path first in
  // forward-only mode and only go for the Map once we notice that we need
  // lots of look ahead. This doesn't handle reversal as well as two ended
  // search but that's unusual. Besides, for the two ended optimization to
  // work on Iterables, we'd need to copy the whole set.

  // In this first iteration, we'll just live with hitting the bad case
  // (adding everything to a Map) in for every insert/move.

  // If you change this code, also update reconcileChildrenIterator() which
  // uses the same algorithm.

  if (__DEV__) {
    // First, validate keys.
    let knownKeys = null;
    for (let i = 0; i < newChildren.length; i++) {
      const child = newChildren[i];
      knownKeys = warnOnInvalidKey(child, knownKeys);
    }
  }

  // 声明一堆的变量
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild; // 上一次渲染的过程中,渲染完成后,当前节点的第一个child节点
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
  // 这个for循环以相同的顺序,分别遍历新老的children对应的节点,判断它的key是否相同
  // 如果不相同,则跳出循环,到这个节点为止
  // 在遍历新老数组的时候,找到第一个不能复用的节点,这时候就会跳出循环
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // 对于react渲染整个数组的过程中,会在每个 Fiber 节点上面 设置一个index属性 就是 这个节点在children里的位置
    // 老的children的Fiber的index > newIdx 说明它们的位置不匹配,则直接赋值给 nextOldFiber
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      // 正常情况下,取当前节点的下一个节点
      nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      expirationTime,
    );
    // 为 null 代表 这个节点不能复用 跳出 for循环
    if (newFiber === null) {
      // TODO: This breaks on empty slots like null children. That's
      // unfortunate because it triggers the slow path all the time. We need
      // a better way to communicate whether this was a miss or null,
      // boolean, undefined, etc.
      // 如果oldFiber 不存在,则处理成下一个节点
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }
    // 存在 newFiber 并且 shouldTrackSideEffects
    if (shouldTrackSideEffects) {
      // newFiber.alternate 不存在,说明它没有复用 oldFiber 来产生一个节点,而是直接 return 了一个新的Fiber
      // 如果重新复用的 oldFiber, 那 newFiber.alternate 应该存在的,这个节点至少经过一次渲染,是有 current 和 workInProgress 的存在的
      // 没有复用之前的节点,则说明 老的节点 失效的状况,则删除之
      if (oldFiber && newFiber.alternate === null) {
        // We matched the slot, but we didn't reuse the existing fiber, so we
        // need to delete the existing child.
        deleteChild(returnFiber, oldFiber);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 每次循环都会给 previousNewFiber 赋值
    // 如果没有被赋值,代表是新节点
    if (previousNewFiber === null) {
      // TODO: Move out of the loop. This only happens for the first run.
      resultingFirstChild = newFiber; // 这里
    } else {
      // TODO: Defer siblings if we're not at the right index for this slot.
      // I.e. if we had null values before, then we want to defer this
      // for each null value. However, we also don't want to call updateSlot
      // with the previous one.
      previousNewFiber.sibling = newFiber; // 如果之前节点已经存在, newFiber 是之前节点previousNewFiber的兄弟节点
    }
    previousNewFiber = newFiber; // 这里进行赋值
    oldFiber = nextOldFiber; // 接着下一轮
  }

  // 跳出循环后,当两者相等,新数组的children 全部创建fiber对象了, 新数组已经操作完成了
  if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber); // 对于老数组情况,存在 oldFiber,删除剩下的节点
    return resultingFirstChild; // 返回第一个节点,第一个节点才是 return fiber 的 child 属性所指向的节点,剩下的后续节点都是通过 .sibling 来指向下去的
  }
  // oldFiber 为 null 时,说明老节点已经被遍历完了
  if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    // 老的节点已经被复用完了,新节点还剩下一部分没有创建,对剩下的节点都进行创建
    // 就不需要关心它是否有复用的节点了
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(
        returnFiber,
        newChildren[newIdx],
        expirationTime,
      );
      if (!newFiber) {
        continue;
      }
      // 同样对这些节点进行 placeChild 操作
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber; // 存在,则处理 sibling 指向
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }

  // Add all children to a key map for quick lookups.
  // 剩下的情况是数组可能存在顺序的变化,oldFiber 可能还有一些兄弟节点,newChildren 还有几个没有被创建
  // 从 oldFiber 剩下的节点中找到 newChildren 可以复用的 Fiber 节点
  // 通过 mapRemainingChildren 来创建一个map
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // Keep scanning and use the map to restore deleted items as moves.
  // 最后的遍历把所有节点创建一遍
  for (; newIdx < newChildren.length; newIdx++) {
    // 调用一个 
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      expirationTime,
    );
    // 存在 newFiber 
    if (newFiber) {
      if (shouldTrackSideEffects) {
        // 存在 current,说明已经被复用了
        if (newFiber.alternate !== null) {
          // The new fiber is a work in progress, but if there exists a
          // current, that means that we reused the fiber. We need to delete
          // it from the child list so that we don't add it to the deletion
          // list.
          // 删除 map 里面的删除该匹配的节点
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
      }
      // 没有被复用, 执行 placeChild
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }

  if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    // 在 existingChildren 中遗留的 fiber 对象执行 删除,因为这些fiber对象没有被复用
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}
  • 下面是上面用到的几个API
    • 进入 updateSlot

      js 复制代码
      // 这个方法对比,新老的key是否相同,来查看它是否可以复用 老的fiber节点
      function updateSlot(
        returnFiber: Fiber,
        oldFiber: Fiber | null,
        newChild: any,
        expirationTime: ExpirationTime,
      ): Fiber | null {
        // Update the fiber if the keys match, otherwise return null.
        // oldFiber 存在的情况下,获取它的key
        const key = oldFiber !== null ? oldFiber.key : null;
        // 文本节点,没有key
        if (typeof newChild === 'string' || typeof newChild === 'number') {
          // Text nodes don't have keys. If the previous node is implicitly keyed
          // we can continue to replace it without aborting even if it is not a text
          // node.
          if (key !== null) {
            return null;
          }
          // 老的节点存在 key
          return updateTextNode(
            returnFiber,
            oldFiber,
            '' + newChild,
            expirationTime,
          );
        }
        // 对象类型,基于 $$typeof 来判断
        if (typeof newChild === 'object' && newChild !== null) {
          switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE: {
              // 只有在前后的key相同的情况下,才会复用节点
              if (newChild.key === key) {
                if (newChild.type === REACT_FRAGMENT_TYPE) {
                  return updateFragment(
                    returnFiber,
                    oldFiber,
                    newChild.props.children,
                    expirationTime,
                    key,
                  );
                }
                return updateElement(
                  returnFiber,
                  oldFiber,
                  newChild,
                  expirationTime,
                );
              } else {
                // 不同,则停止,不能复用
                return null;
              }
            }
            // 下面也类似
            case REACT_PORTAL_TYPE: {
              if (newChild.key === key) {
                return updatePortal(
                  returnFiber,
                  oldFiber,
                  newChild,
                  expirationTime,
                );
              } else {
                return null;
              }
            }
          }
          // 继续判断是数组还是可迭代
          if (isArray(newChild) || getIteratorFn(newChild)) {
            if (key !== null) {
              return null;
            }
      
            return updateFragment(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
              null,
            );
          }
      
          throwOnInvalidObjectType(returnFiber, newChild);
        }
      
        if (__DEV__) {
          if (typeof newChild === 'function') {
            warnOnFunctionType();
          }
        }
      
        return null;
      }
    • 进入 placeChild

      js 复制代码
      function placeChild(
        newFiber: Fiber,
        lastPlacedIndex: number,
        newIndex: number,
      ): number {
        // 同步 index
        newFiber.index = newIndex;
        if (!shouldTrackSideEffects) {
          // Noop.
          return lastPlacedIndex;
        }
        // 获取 current
        const current = newFiber.alternate;
        // 存在 current
        if (current !== null) {
          const oldIndex = current.index;
          if (oldIndex < lastPlacedIndex) {
            // This is a move.
            newFiber.effectTag = Placement; // 代表这个节点要被挂载到 dom上面 顺序变化了,说明这个节点被移动了,进行 dom 操作,tag 就是 Placement
            return lastPlacedIndex;
          } else {
            // This item can stay in place.
            return oldIndex; // 存在于原来的位置
          }
        } else {
          // current 为 null 说明节点没有被渲染过,它是一个插入的节点
          // 插入的节点也是需要使用 Placement 同样进行dom操作
          // 就是这个节点要根据某个顺序插入到 dom节点的后面
          // This is an insertion.
          newFiber.effectTag = Placement;
          return lastPlacedIndex;
        }
      }
    • 进入 mapRemainingChildren

      js 复制代码
      function mapRemainingChildren(
        returnFiber: Fiber,
        currentFirstChild: Fiber,
      ): Map<string | number, Fiber> {
        // Add the remaining children to a temporary map so that we can find them by
        // keys quickly. Implicit (null) keys get added to this set with their index
        // instead.
        // 通过 Map 对象找到 key相同的节点,判断是否可以复用
        const existingChildren: Map<string | number, Fiber> = new Map();
      
        let existingChild = currentFirstChild;
        // 遍历剩下的节点,获取其key 
        while (existingChild !== null) {
          if (existingChild.key !== null) {
            // set key value
            existingChildren.set(existingChild.key, existingChild);
          } else {
            // key 不存在,使用 index
            existingChildren.set(existingChild.index, existingChild);
          }
          existingChild = existingChild.sibling;
        }
        return existingChildren;
      }
    • 进入 updateFromMap

      js 复制代码
      function updateFromMap(
        existingChildren: Map<string | number, Fiber>,
        returnFiber: Fiber,
        newIdx: number,
        newChild: any,
        expirationTime: ExpirationTime,
      ): Fiber | null {
        // 匹配 文本节点
        if (typeof newChild === 'string' || typeof newChild === 'number') {
          // Text nodes don't have keys, so we neither have to check the old nor
          // new node for the key. If both are text nodes, they match.
          const matchedFiber = existingChildren.get(newIdx) || null; // 通过 newIdx 来查找,不管是否找到
          // 返回一个text node
          return updateTextNode(
            returnFiber,
            matchedFiber,
            '' + newChild,
            expirationTime,
          );
        }
        // 这里和 updateSlot类似
        if (typeof newChild === 'object' && newChild !== null) {
          switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE: {
              const matchedFiber =
                existingChildren.get(
                  newChild.key === null ? newIdx : newChild.key,
                ) || null;
              if (newChild.type === REACT_FRAGMENT_TYPE) {
                return updateFragment(
                  returnFiber,
                  matchedFiber,
                  newChild.props.children,
                  expirationTime,
                  newChild.key,
                );
              }
              return updateElement(
                returnFiber,
                matchedFiber,
                newChild,
                expirationTime,
              );
            }
            case REACT_PORTAL_TYPE: {
              const matchedFiber =
                existingChildren.get(
                  newChild.key === null ? newIdx : newChild.key,
                ) || null;
              return updatePortal(
                returnFiber,
                matchedFiber,
                newChild,
                expirationTime,
              );
            }
          }
      
          if (isArray(newChild) || getIteratorFn(newChild)) {
            const matchedFiber = existingChildren.get(newIdx) || null;
            return updateFragment(
              returnFiber,
              matchedFiber,
              newChild,
              expirationTime,
              null,
            );
          }
      
          throwOnInvalidObjectType(returnFiber, newChild);
        }
      
        if (__DEV__) {
          if (typeof newChild === 'function') {
            warnOnFunctionType();
          }
        }
      
        return null;
      }

在 reconcileChildFibers 函数中调用的 reconcileChildrenIterator

定位到 packages/react-reconciler/src/ReactChildFiber.js#L891

js 复制代码
function reconcileChildrenIterator(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildrenIterable: Iterable<*>,
  expirationTime: ExpirationTime,
): Fiber | null {
  // This is the same implementation as reconcileChildrenArray(),
  // but using the iterator instead.
  
  // 获取 iteratorFn
  const iteratorFn = getIteratorFn(newChildrenIterable);
  invariant(
    typeof iteratorFn === 'function',
    'An object is not an iterable. This error is likely caused by a bug in ' +
      'React. Please file an issue.',
  );

  if (__DEV__) {
    // We don't support rendering Generators because it's a mutation.
    // See https://github.com/facebook/react/issues/12995
    if (
      typeof Symbol === 'function' &&
      // $FlowFixMe Flow doesn't know about toStringTag
      newChildrenIterable[Symbol.toStringTag] === 'Generator'
    ) {
      warning(
        didWarnAboutGenerators,
        'Using Generators as children is unsupported and will likely yield ' +
          'unexpected results because enumerating a generator mutates it. ' +
          'You may convert it to an array with `Array.from()` or the ' +
          '`[...spread]` operator before rendering. Keep in mind ' +
          'you might need to polyfill these features for older browsers.',
      );
      didWarnAboutGenerators = true;
    }

    // Warn about using Maps as children
    if ((newChildrenIterable: any).entries === iteratorFn) {
      warning(
        didWarnAboutMaps,
        'Using Maps as children is unsupported and will likely yield ' +
          'unexpected results. Convert it to a sequence/iterable of keyed ' +
          'ReactElements instead.',
      );
      didWarnAboutMaps = true;
    }

    // First, validate keys.
    // We'll get a different iterator later for the main pass.
    // 获取 newChildren,这里 newChildrenIterable 是具有 迭代特性的 children
    const newChildren = iteratorFn.call(newChildrenIterable);
    if (newChildren) {
      let knownKeys = null;
      let step = newChildren.next();
      for (; !step.done; step = newChildren.next()) {
        const child = step.value;
        knownKeys = warnOnInvalidKey(child, knownKeys);
      }
    }
  }

  const newChildren = iteratorFn.call(newChildrenIterable);
  invariant(newChildren != null, 'An iterable object provided no iterator.');

  // 声明很多变量
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
  
  // 通过 next 向下获取新节点
  let step = newChildren.next();
  // 这个 for 循环的判断是基于迭代器的,当遍历结束 .done 返回的是 false
  // step 都是 next,在 Fiber 中的key 如果不存在,使用 index
  // 这里也要模拟一个 index 出来
  for (
    ;
    oldFiber !== null && !step.done;
    newIdx++, step = newChildren.next()
  ) {
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      step.value,
      expirationTime,
    );
    if (newFiber === null) {
      // TODO: This breaks on empty slots like null children. That's
      // unfortunate because it triggers the slow path all the time. We need
      // a better way to communicate whether this was a miss or null,
      // boolean, undefined, etc.
      if (!oldFiber) {
        oldFiber = nextOldFiber;
      }
      break;
    }
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        // We matched the slot, but we didn't reuse the existing fiber, so we
        // need to delete the existing child.
        deleteChild(returnFiber, oldFiber);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      // TODO: Move out of the loop. This only happens for the first run.
      resultingFirstChild = newFiber;
    } else {
      // TODO: Defer siblings if we're not at the right index for this slot.
      // I.e. if we had null values before, then we want to defer this
      // for each null value. However, we also don't want to call updateSlot
      // with the previous one.
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }

  if (step.done) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    for (; !step.done; newIdx++, step = newChildren.next()) {
      const newFiber = createChild(returnFiber, step.value, expirationTime);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }

  // Add all children to a key map for quick lookups.
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // Keep scanning and use the map to restore deleted items as moves.
  for (; !step.done; newIdx++, step = newChildren.next()) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      step.value,
      expirationTime,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // The new fiber is a work in progress, but if there exists a
          // current, that means that we reused the fiber. We need to delete
          // it from the child list so that we don't add it to the deletion
          // list.
          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) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}
  • 这个 reconcileChildIterator 方法和上面的 reconcileChildrenArray 基本一致
  • 就是判断条件不同,不再赘述
  • 上述两个 API 是 对可遍历的 children 调和的一个过程
  • 目的是尽量的复用可复用的 fiber 节点
  • 减少对象声明和内存回收的过程
相关推荐
2401_857617626 分钟前
“无缝购物体验”:跨平台网上购物商城的设计与实现
java·开发语言·前端·安全·架构·php
2401_8574396915 分钟前
智慧社区电商系统:提升用户体验的界面设计
前端·javascript·php·ux
我是高手高手高高手24 分钟前
ThinkPHP8多应用配置及不同域名访问不同应用的配置
linux·服务器·前端·php
小李小李不讲道理27 分钟前
行动+思考 | 2024年度总结
前端·程序员·年终总结
csdnLN1 小时前
$.ajax() 对应事件done() 、fail()、always() 的用法
前端·javascript·ajax
甜味橘阳1 小时前
echarts地图可视化展示
前端·javascript·echarts
bloxed2 小时前
前端文件下载多方式集合
前端·filedownload
余生H2 小时前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC2 小时前
CSS(四)display和float
前端·css
cwtlw2 小时前
CSS学习记录20
前端·css·笔记·学习